@hamedb89/localghost 0.1.8 → 0.1.10

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,261 @@ 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 getDisplayDefaults(config, defaults) {
499
+ return config.mode === "public" && !config.preview ? void 0 : defaults;
500
+ }
501
+ function createDisplayUrl(config, defaults, domain) {
502
+ const input2 = getPreviewDefaults(config.preview, getDisplayDefaults(config, defaults));
503
+ const protocol = input2.protocol ?? "https";
504
+ const slug = createNamespaceDisplaySlug(config.namespace, getDisplayValues(input2));
505
+ const entryHost = domain ? getGhostTunnelEntryHost(domain, config) : input2.domain ? getGhostTunnelEntryHost(input2.domain, config) : `${config.subdomain}.*`;
506
+ const url = `${protocol}://${slug}.${entryHost}/`;
507
+ if (!input2.path) return url;
508
+ return `${url}${input2.path.replace(/^\/+/, "")}`;
509
+ }
510
+ function createDisplayUrls(config, defaults) {
511
+ const displayDefaults = getDisplayDefaults(config, defaults);
512
+ const domains = config.domains.length > 0 ? config.domains : displayDefaults?.domain ? [displayDefaults.domain] : [];
513
+ const urls = domains.length > 0 ? domains.map((domain) => createDisplayUrl(config, displayDefaults, domain)) : [createDisplayUrl(config, displayDefaults)];
514
+ return [...new Set(urls)];
515
+ }
516
+ function maybeConstructPreviewUrl(config, defaults) {
517
+ if (!config.preview) return void 0;
518
+ const input2 = getPreviewDefaults(config.preview, defaults);
519
+ if (!input2.domain || !input2.route || !input2.project || !input2.owner) return void 0;
520
+ return constructGhostTunnelUrl({
521
+ domain: input2.domain,
522
+ route: input2.route,
523
+ project: input2.project,
524
+ owner: input2.owner,
525
+ values: input2.values,
526
+ ...input2.path ? { path: input2.path } : {},
527
+ ...input2.protocol ? { protocol: input2.protocol } : {},
528
+ ghostTunnel: config
529
+ });
530
+ }
531
+ function resolveGhostTunnelConfig(options, defaults) {
532
+ if (options === false || typeof options === "undefined") {
533
+ return {
534
+ enabled: false,
535
+ mode: DEFAULT_GHOST_TUNNEL_MODE,
536
+ domains: [],
537
+ subdomain: DEFAULT_GHOST_TUNNEL_SUBDOMAIN,
538
+ namespace: resolveNamespaceConfig(void 0),
539
+ displayUrls: [],
540
+ requireHttps: true,
541
+ requireAuth: true
542
+ };
543
+ }
544
+ const config = typeof options === "string" ? { mode: options } : options;
545
+ const subdomain = config.subdomain ?? DEFAULT_GHOST_TUNNEL_SUBDOMAIN;
546
+ assertValidSubdomain(subdomain);
547
+ const domains = normalizeDomains(config.domains);
548
+ const enabled = config.enabled ?? true;
549
+ const resolved = {
550
+ enabled,
551
+ mode: parseGhostTunnelMode(config.mode),
552
+ domains,
553
+ subdomain,
554
+ namespace: resolveNamespaceConfig(config.namespace),
555
+ ...config.preview ? { preview: config.preview } : {},
556
+ displayUrls: [],
557
+ requireHttps: config.requireHttps ?? true,
558
+ requireAuth: config.requireAuth ?? true
559
+ };
560
+ if (!enabled) {
561
+ return resolved;
562
+ }
563
+ const previewUrl = maybeConstructPreviewUrl(resolved, defaults);
564
+ const displayUrls = previewUrl ? [previewUrl] : createDisplayUrls(resolved, defaults);
565
+ return {
566
+ ...resolved,
567
+ displayUrls,
568
+ ...displayUrls[0] ? { displayUrl: displayUrls[0] } : {},
569
+ ...previewUrl ? { previewUrl } : {}
570
+ };
571
+ }
572
+ function getGhostTunnelEntryHost(domain, options = {}) {
573
+ const config = toGhostTunnelConfig(options);
574
+ const normalizedDomain = normalizeDomain(domain);
575
+ if (!normalizedDomain) {
576
+ throw new Error(`Invalid ghost tunnel domain: ${domain}`);
577
+ }
578
+ return `${config.subdomain}.${normalizedDomain}`;
579
+ }
580
+ function constructGhostTunnelHost(input2) {
581
+ const config = toGhostTunnelConfig(input2.ghostTunnel ?? {});
582
+ if (!config.enabled) {
583
+ throw new Error("Ghost tunnel is not enabled.");
584
+ }
585
+ const namespaceValues = {
586
+ route: input2.route,
587
+ project: input2.project,
588
+ owner: input2.owner,
589
+ ...input2.values ?? {}
590
+ };
591
+ const slug = createNamespaceSlug(config.namespace, namespaceValues);
592
+ return `${slug}.${getGhostTunnelEntryHost(input2.domain, config)}`;
593
+ }
594
+ function constructGhostTunnelUrl(input2) {
595
+ const protocol = input2.protocol ?? "https";
596
+ const host = constructGhostTunnelHost(input2);
597
+ const url = new URL(`${protocol}://${host}/`);
598
+ if (input2.path) {
599
+ url.pathname = `/${input2.path.replace(/^\/+/, "")}`;
600
+ }
601
+ if (input2.searchParams instanceof URLSearchParams) {
602
+ url.search = input2.searchParams.toString();
603
+ } else if (input2.searchParams) {
604
+ for (const [key, value] of Object.entries(input2.searchParams)) {
605
+ if (typeof value !== "undefined" && value !== null) {
606
+ url.searchParams.set(key, String(value));
607
+ }
608
+ }
609
+ }
610
+ return url.toString();
611
+ }
612
+
320
613
  // src/context.ts
321
614
  var LOCALGHOST_PROJECT_CONFIG_FILES = [
322
615
  "localghost.config.mjs",
@@ -341,6 +634,25 @@ function envHttps() {
341
634
  if (!value) return void 0;
342
635
  return ["1", "true", "yes", "on"].includes(value.toLowerCase());
343
636
  }
637
+ function getPackageName(cwd) {
638
+ try {
639
+ const pkg = JSON.parse(readFileSync4(join4(cwd, "package.json"), "utf8"));
640
+ return typeof pkg.name === "string" ? pkg.name : void 0;
641
+ } catch {
642
+ return void 0;
643
+ }
644
+ }
645
+ function getPackageOwner(cwd) {
646
+ const packageName = getPackageName(cwd);
647
+ if (!packageName?.startsWith("@")) return void 0;
648
+ return packageName.slice(1).split("/")[0];
649
+ }
650
+ function getLocalOwner(cwd) {
651
+ return sanitizeProjectName(process.env.LOCALGHOST_OWNER ?? getPackageOwner(cwd) ?? process.env.USER ?? process.env.USERNAME ?? "local");
652
+ }
653
+ function getRouteName(primaryHost, fallback) {
654
+ return sanitizeProjectName(primaryHost.split(".")[0] ?? fallback);
655
+ }
344
656
  function readOptionsFromContext(options) {
345
657
  return {
346
658
  cwd: options.cwd ?? process.cwd(),
@@ -379,18 +691,22 @@ function addDefaultWwwAliases(entries) {
379
691
  function defined(input2) {
380
692
  return Object.fromEntries(Object.entries(input2).filter(([, value]) => typeof value !== "undefined"));
381
693
  }
382
- async function readProjectConfig(cwd, configFile) {
383
- if (configFile === false) return {};
384
- const candidates = configFile ? [configFile] : LOCALGHOST_PROJECT_CONFIG_FILES;
694
+ async function readLocalghostProjectConfig(options = {}) {
695
+ const cwd = options.cwd ?? process.cwd();
696
+ if (options.configFile === false) return { config: {} };
697
+ const candidates = options.configFile ? [options.configFile] : LOCALGHOST_PROJECT_CONFIG_FILES;
385
698
  const path = candidates.map((candidate) => resolveDevHostsPath({ cwd, fileName: candidate }).path).find((candidate) => existsSync3(candidate));
386
- if (!path) return {};
699
+ if (!path) return { config: {} };
387
700
  const imported = await import(`${pathToFileURL(path).href}?localghost=${Date.now()}`);
388
701
  const config = imported.default ?? imported;
389
702
  return { config, path };
390
703
  }
391
704
  async function resolveLocalghostContext(options = {}) {
392
705
  const cwd = options.cwd ?? process.cwd();
393
- const projectConfig = await readProjectConfig(cwd, options.localghostConfig);
706
+ const projectConfig = await readLocalghostProjectConfig({
707
+ cwd,
708
+ ...typeof options.localghostConfig !== "undefined" ? { configFile: options.localghostConfig } : {}
709
+ });
394
710
  const merged = {
395
711
  ...projectConfig.config,
396
712
  ...defined(options)
@@ -399,7 +715,7 @@ async function resolveLocalghostContext(options = {}) {
399
715
  const resolvedPath = resolveDevHostsPath(readOptions);
400
716
  const configEntries = readDevHosts(readOptions);
401
717
  const requestedPort = merged.port ?? envPort() ?? configEntries[0]?.port ?? 5173;
402
- const dynamicPort = merged.dynamicPort ?? envDynamicPort() ?? false;
718
+ const dynamicPort = merged.dynamicPort ?? envDynamicPort() ?? true;
403
719
  const bindHost = merged.bindHost ?? "127.0.0.1";
404
720
  const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
405
721
  const port = dynamicPort ? await findAvailablePort(requestedPort, { host: probeHost }) : requestedPort;
@@ -407,9 +723,15 @@ async function resolveLocalghostContext(options = {}) {
407
723
  const entries = wwwAlias ? addDefaultWwwAliases(withRuntimePort(configEntries, requestedPort, port)) : withRuntimePort(configEntries, requestedPort, port);
408
724
  const hosts = uniqueHosts(entries);
409
725
  const primaryHost = merged.primaryHost ?? entries.find((entry) => entry.port === port)?.host ?? hosts[0] ?? `${sanitizeProjectName(getProjectName(cwd))}.localhost`;
726
+ const projectName = sanitizeProjectName(merged.project ?? getProjectName(cwd));
727
+ const ghostTunnel = resolveGhostTunnelConfig(merged.ghostTunnel, {
728
+ route: getRouteName(primaryHost, projectName),
729
+ project: projectName,
730
+ owner: getLocalOwner(cwd)
731
+ });
410
732
  return {
411
733
  cwd,
412
- projectName: sanitizeProjectName(merged.project ?? getProjectName(cwd)),
734
+ projectName,
413
735
  readOptions,
414
736
  configPath: resolvedPath.path,
415
737
  configFileName: resolvedPath.fileName,
@@ -423,6 +745,7 @@ async function resolveLocalghostContext(options = {}) {
423
745
  primaryHost,
424
746
  https: merged.https ?? envHttps() ?? false,
425
747
  wwwAlias,
748
+ ghostTunnel,
426
749
  ...projectConfig.path ? { projectConfigPath: projectConfig.path } : {}
427
750
  };
428
751
  }
@@ -473,7 +796,7 @@ function assertLocalDevelopment(command, env = process.env) {
473
796
  // src/hosts-file.ts
474
797
  import { writeFileSync as writeFileSync3 } from "fs";
475
798
  import { tmpdir } from "os";
476
- import { join as join4 } from "path";
799
+ import { join as join5 } from "path";
477
800
  import { execa as execa3 } from "execa";
478
801
  function escapeRegExp(value) {
479
802
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -515,7 +838,7 @@ function removeManagedBlock(existing, projectName) {
515
838
  }
516
839
  async function writeSystemHostsFile(hostsPath, next, projectName) {
517
840
  const sanitizedProjectName = sanitizeProjectName(projectName);
518
- const tempPath = join4(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
841
+ const tempPath = join5(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
519
842
  writeFileSync3(tempPath, next, "utf8");
520
843
  if (process.platform === "win32") {
521
844
  throw new Error(`Windows support: run as administrator and copy ${tempPath} to ${hostsPath}.`);
@@ -548,11 +871,11 @@ async function removeSystemHosts(projectName) {
548
871
  }
549
872
 
550
873
  // src/init.ts
551
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
552
- import { join as join5 } from "path";
874
+ import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
875
+ import { join as join6 } from "path";
553
876
  function detectPackageManager(cwd = process.cwd()) {
554
- if (existsSync4(join5(cwd, "pnpm-lock.yaml"))) return "pnpm";
555
- if (existsSync4(join5(cwd, "yarn.lock"))) return "yarn";
877
+ if (existsSync4(join6(cwd, "pnpm-lock.yaml"))) return "pnpm";
878
+ if (existsSync4(join6(cwd, "yarn.lock"))) return "yarn";
556
879
  return "npm";
557
880
  }
558
881
  function packageRunCommand(packageManager, script) {
@@ -572,7 +895,7 @@ function renderConfig(options) {
572
895
  }
573
896
  function readPackageJson(path) {
574
897
  try {
575
- return JSON.parse(readFileSync4(path, "utf8"));
898
+ return JSON.parse(readFileSync5(path, "utf8"));
576
899
  } catch {
577
900
  return null;
578
901
  }
@@ -624,7 +947,7 @@ function initLocalghost(options = {}) {
624
947
  const apiPort = options.apiPort ?? 8787;
625
948
  const packageManager = options.packageManager ?? detectPackageManager(cwd);
626
949
  const configFile = options.configFile ?? LOCALGHOST_CONFIG_FILE;
627
- const configPath = join5(cwd, configFile);
950
+ const configPath = join6(cwd, configFile);
628
951
  const configExists = existsSync4(configPath);
629
952
  if (configExists && !options.force) {
630
953
  return {
@@ -641,7 +964,7 @@ function initLocalghost(options = {}) {
641
964
  };
642
965
  }
643
966
  writeTextFile(configPath, renderConfig({ host, port, apiHost, apiPort }));
644
- const packageJsonPath = join5(cwd, "package.json");
967
+ const packageJsonPath = join6(cwd, "package.json");
645
968
  const packageJsonChanged = options.writeScripts ? updatePackageScripts(packageJsonPath, configFile) : false;
646
969
  return {
647
970
  configPath,
@@ -682,6 +1005,20 @@ async function confirm(question, defaultValue = true) {
682
1005
  }
683
1006
 
684
1007
  // src/routes.ts
1008
+ var ansi = {
1009
+ cyan: "\x1B[36m",
1010
+ dim: "\x1B[2m",
1011
+ green: "\x1B[32m",
1012
+ reset: "\x1B[0m",
1013
+ yellow: "\x1B[33m"
1014
+ };
1015
+ function colorize(value, color, enabled) {
1016
+ return enabled ? `${color}${value}${ansi.reset}` : value;
1017
+ }
1018
+ function colorizeUrl(value, enabled) {
1019
+ if (!enabled) return value;
1020
+ return colorize(value.replace(/\*/g, `${ansi.yellow}*${ansi.cyan}`), ansi.cyan, enabled);
1021
+ }
685
1022
  function getDomainRoutes(entries, options = {}) {
686
1023
  const protocol = options.https === true ? "https" : "http";
687
1024
  return [...entries].sort((left, right) => left.host.localeCompare(right.host) || left.port - right.port).map((entry) => ({
@@ -701,13 +1038,40 @@ function formatDomainRoutes(entries, options = {}) {
701
1038
  ...routes.map((route) => ` ${route.url} -> ${route.upstream}`)
702
1039
  ].join("\n");
703
1040
  }
1041
+ function formatGhostTunnel(config, options = {}) {
1042
+ if (!config.enabled) return null;
1043
+ const color = options.color === true;
1044
+ const label = options.label ?? "expected";
1045
+ const labelColor = label === "running" ? ansi.green : ansi.dim;
1046
+ const lines = [
1047
+ "localghost ghost tunnel",
1048
+ ` mode: ${config.mode}`
1049
+ ];
1050
+ const urls = config.displayUrls.length > 0 ? config.displayUrls : config.displayUrl ? [config.displayUrl] : [];
1051
+ if (urls.length === 0) {
1052
+ lines.push(` ${label}: unavailable`);
1053
+ } else if (urls.length === 1) {
1054
+ lines.push(` ${colorize(label, labelColor, color)}: ${colorizeUrl(urls[0], color)}`);
1055
+ } else {
1056
+ lines.push(` ${colorize(label, labelColor, color)}:`);
1057
+ for (const url of urls) {
1058
+ lines.push(` ${colorizeUrl(url, color)}`);
1059
+ }
1060
+ }
1061
+ if (options.verbose) {
1062
+ lines.push(` domains: ${config.domains.length > 0 ? config.domains.join(", ") : "*"}`);
1063
+ lines.push(` access: ${config.requireAuth ? "auth required" : "app decides"}`);
1064
+ lines.push(` transport: ${config.requireHttps ? "https required" : "http allowed"}`);
1065
+ }
1066
+ return lines.join("\n");
1067
+ }
704
1068
 
705
1069
  // src/state.ts
706
1070
  import { existsSync as existsSync5 } from "fs";
707
- import { join as join6 } from "path";
1071
+ import { join as join7 } from "path";
708
1072
  var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
709
1073
  function getLocalghostStatePath(cwd = process.cwd()) {
710
- return join6(cwd, LOCALGHOST_STATE_FILE);
1074
+ return join7(cwd, LOCALGHOST_STATE_FILE);
711
1075
  }
712
1076
  function readLocalghostState(cwd = process.cwd()) {
713
1077
  const path = getLocalghostStatePath(cwd);
@@ -727,11 +1091,11 @@ function patchLocalghostState(cwd, patch) {
727
1091
  }
728
1092
 
729
1093
  // src/update-check.ts
730
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
1094
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
731
1095
  import { homedir as homedir2 } from "os";
732
- import { dirname as dirname4, join as join7 } from "path";
1096
+ import { dirname as dirname4, join as join8 } from "path";
733
1097
  var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
734
- var LOCALGHOST_VERSION = "0.1.8";
1098
+ var LOCALGHOST_VERSION = "0.1.10";
735
1099
  var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
736
1100
  var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
737
1101
  var UPDATE_CHECK_TIMEOUT_MS = 900;
@@ -743,13 +1107,13 @@ function isUpdateCheckDisabled(env = process.env) {
743
1107
  }
744
1108
  function getUpdateCheckCachePath(env = process.env) {
745
1109
  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");
1110
+ const cacheRoot = env.XDG_CACHE_HOME || join8(homedir2(), ".cache");
1111
+ return join8(cacheRoot, "localghost", "update-check.json");
748
1112
  }
749
1113
  function readCache(path = getUpdateCheckCachePath()) {
750
1114
  if (!existsSync6(path)) return null;
751
1115
  try {
752
- return JSON.parse(readFileSync5(path, "utf8"));
1116
+ return JSON.parse(readFileSync6(path, "utf8"));
753
1117
  } catch {
754
1118
  return null;
755
1119
  }
@@ -903,8 +1267,18 @@ function warnAboutLocalMdns(entries) {
903
1267
  );
904
1268
  }
905
1269
  }
1270
+ function shouldColor() {
1271
+ return process.stdout.isTTY && !process.env.NO_COLOR;
1272
+ }
906
1273
  function logDomainRoutes(entries, options = {}) {
907
1274
  console.log(formatDomainRoutes(entries, options));
1275
+ if (options.ghostTunnel?.enabled) {
1276
+ console.log(formatGhostTunnel(options.ghostTunnel, {
1277
+ color: shouldColor(),
1278
+ label: "expected",
1279
+ verbose: options.verbose === true
1280
+ }));
1281
+ }
908
1282
  }
909
1283
  function parsePort2(value) {
910
1284
  const port = Number.parseInt(value, 10);
@@ -998,7 +1372,7 @@ function getSetupReadiness(options) {
998
1372
  }
999
1373
  const hostsPath = getSystemHostsPath();
1000
1374
  try {
1001
- const hosts = readFileSync6(hostsPath, "utf8");
1375
+ const hosts = readFileSync7(hostsPath, "utf8");
1002
1376
  const expectedHostsBlock = renderHostsBlock(projectName, entries).trimEnd();
1003
1377
  if (!hosts.includes(expectedHostsBlock)) {
1004
1378
  reasons.push(`The Localghost hosts block in ${hostsPath} is missing or stale.`);
@@ -1012,7 +1386,7 @@ function getSetupReadiness(options) {
1012
1386
  reasons.push(`Missing Caddyfile at ${caddyfilePath}.`);
1013
1387
  } else {
1014
1388
  const expectedCaddyfile = renderCaddyfile(entries, { https });
1015
- const currentCaddyfile = readFileSync6(caddyfilePath, "utf8");
1389
+ const currentCaddyfile = readFileSync7(caddyfilePath, "utf8");
1016
1390
  if (currentCaddyfile !== expectedCaddyfile) {
1017
1391
  reasons.push(`Caddyfile at ${caddyfilePath} is stale for ${https ? "HTTPS" : "HTTP"} mode.`);
1018
1392
  }
@@ -1047,6 +1421,14 @@ async function runSetupFromReadiness(cwd, https, readiness) {
1047
1421
  ...existingTrustMarkers(cwd),
1048
1422
  entries: readiness.entries
1049
1423
  });
1424
+ registerLocalghostSetup({
1425
+ cwd,
1426
+ projectName: readiness.projectName,
1427
+ configPath: readiness.configPath,
1428
+ caddyfilePath,
1429
+ https,
1430
+ entries: readiness.entries
1431
+ });
1050
1432
  }
1051
1433
  function wait(ms) {
1052
1434
  return new Promise((resolve2) => setTimeout(resolve2, ms));
@@ -1096,35 +1478,91 @@ function registerCleanup(id) {
1096
1478
  process.off("exit", cleanup);
1097
1479
  };
1098
1480
  }
1099
- async function getRunView(run) {
1481
+ async function getRouteViews(entries) {
1100
1482
  const portStatus = /* @__PURE__ */ new Map();
1101
- for (const entry of run.entries) {
1483
+ for (const entry of entries) {
1102
1484
  if (!portStatus.has(entry.port)) {
1103
1485
  portStatus.set(entry.port, !await isPortAvailable(entry.port));
1104
1486
  }
1105
1487
  }
1488
+ return entries.map((entry) => ({
1489
+ host: entry.host,
1490
+ port: entry.port,
1491
+ target: `127.0.0.1:${entry.port}`,
1492
+ listening: portStatus.get(entry.port) ?? false
1493
+ }));
1494
+ }
1495
+ function setupKey(input2) {
1496
+ return `${input2.projectName}:${input2.cwd}:${input2.configPath ?? ""}`;
1497
+ }
1498
+ function runKey(input2) {
1499
+ return `${input2.projectName}:${input2.cwd}:${input2.configPath ?? ""}`;
1500
+ }
1501
+ async function getInstanceViews(setups, runs) {
1502
+ const runBySetup = new Map(runs.map((run) => [runKey(run), run]));
1503
+ const instances = [];
1504
+ for (const setup of setups) {
1505
+ const run = runBySetup.get(setupKey(setup));
1506
+ if (run) {
1507
+ instances.push(await getRunInstanceView(run, setup));
1508
+ runBySetup.delete(setupKey(setup));
1509
+ continue;
1510
+ }
1511
+ instances.push({
1512
+ id: setup.id,
1513
+ cwd: setup.cwd,
1514
+ projectName: setup.projectName,
1515
+ running: false,
1516
+ mode: "setup",
1517
+ updatedAt: setup.updatedAt,
1518
+ ...setup.configPath ? { configPath: setup.configPath } : {},
1519
+ ...setup.caddyfilePath ? { caddyfilePath: setup.caddyfilePath } : {},
1520
+ ...typeof setup.https === "boolean" ? { https: setup.https } : {},
1521
+ routes: await getRouteViews(setup.entries)
1522
+ });
1523
+ }
1524
+ for (const run of runBySetup.values()) {
1525
+ instances.push(await getRunInstanceView(run));
1526
+ }
1527
+ return instances.sort((left, right) => {
1528
+ if (left.running !== right.running) return left.running ? -1 : 1;
1529
+ return left.projectName.localeCompare(right.projectName);
1530
+ });
1531
+ }
1532
+ async function getRunInstanceView(run, setup) {
1106
1533
  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
- }))
1534
+ id: setup?.id ?? run.id,
1535
+ cwd: run.cwd,
1536
+ projectName: run.projectName,
1537
+ running: true,
1538
+ mode: run.mode,
1539
+ updatedAt: setup?.updatedAt ?? run.updatedAt,
1540
+ startedAt: run.startedAt,
1541
+ pid: run.pid,
1542
+ ...run.caddyPid ? { caddyPid: run.caddyPid } : {},
1543
+ ...run.childPid ? { childPid: run.childPid } : {},
1544
+ ...run.childCommand ? { childCommand: run.childCommand } : {},
1545
+ ...run.configPath ? { configPath: run.configPath } : {},
1546
+ ...run.caddyfilePath ? { caddyfilePath: run.caddyfilePath } : {},
1547
+ ...typeof run.https === "boolean" ? { https: run.https } : {},
1548
+ routes: await getRouteViews(run.entries)
1114
1549
  };
1115
1550
  }
1116
- function formatRunViews(runs) {
1117
- if (runs.length === 0) return "No Localghost apps are running.";
1551
+ function formatInstanceViews(instances) {
1552
+ if (instances.length === 0) return "No Localghost setups found.";
1118
1553
  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;
1554
+ for (const instance of instances) {
1555
+ const command = instance.childCommand?.length ? ` ${instance.childCommand.join(" ")}` : "";
1556
+ const mode = command ? `${instance.mode}:${command}` : instance.mode === "setup" ? "" : instance.mode;
1122
1557
  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) {
1558
+ lines.push(`${instance.projectName} ${instance.running ? "running" : "setup"}${mode ? ` ${mode}` : ""}`);
1559
+ lines.push(` cwd: ${instance.cwd}`);
1560
+ if (instance.pid) {
1561
+ lines.push(` pid: ${instance.pid}${instance.caddyPid ? `, caddy: ${instance.caddyPid}` : ""}${instance.childPid ? `, child: ${instance.childPid}` : ""}`);
1562
+ }
1563
+ if (instance.startedAt) lines.push(` started: ${instance.startedAt}`);
1564
+ if (!instance.startedAt && instance.updatedAt) lines.push(` setup: ${instance.updatedAt}`);
1565
+ for (const route of instance.routes) {
1128
1566
  lines.push(` ${route.host} -> ${route.target} (${route.listening ? "listening" : "not listening"})`);
1129
1567
  }
1130
1568
  }
@@ -1198,7 +1636,7 @@ program.command("setup").description("Update /etc/hosts and generate/validate Ca
1198
1636
  const configPath = context.configPath;
1199
1637
  const entries = context.entries;
1200
1638
  warnAboutLocalMdns(entries);
1201
- logDomainRoutes(entries, { https });
1639
+ logDomainRoutes(entries, { https, ghostTunnel: context.ghostTunnel });
1202
1640
  explainHostsPassword();
1203
1641
  const hostsResult = await updateSystemHosts(projectName, entries);
1204
1642
  if (hostsResult.changed) {
@@ -1221,6 +1659,14 @@ program.command("setup").description("Update /etc/hosts and generate/validate Ca
1221
1659
  ...existingTrustMarkers(options.cwd),
1222
1660
  entries
1223
1661
  });
1662
+ registerLocalghostSetup({
1663
+ cwd: options.cwd,
1664
+ projectName,
1665
+ configPath,
1666
+ caddyfilePath: caddyfile,
1667
+ https,
1668
+ entries
1669
+ });
1224
1670
  console.log(`Generated ${caddyfile}`);
1225
1671
  console.log(`Mode ${https ? "HTTPS" : "HTTP"}`);
1226
1672
  console.log(`State ${statePath}`);
@@ -1234,7 +1680,7 @@ program.command("trust").description("Trust Caddy's local HTTPS CA for this proj
1234
1680
  throw new Error("Localghost HTTPS is not enabled for this context. Set https: true in localghost.config.mjs or pass --https.");
1235
1681
  }
1236
1682
  warnAboutLocalMdns(context.entries);
1237
- logDomainRoutes(context.entries, { https: true });
1683
+ logDomainRoutes(context.entries, { https: true, ghostTunnel: context.ghostTunnel });
1238
1684
  explainTrustPassword();
1239
1685
  const caddyfile = await writeCaddyfile(context.entries, options.cwd, { https: true });
1240
1686
  await validateCaddyfile(caddyfile);
@@ -1264,6 +1710,7 @@ program.command("reset").description("Remove Localghost setup state without dele
1264
1710
  } else {
1265
1711
  console.log(`No Localghost hosts block found in ${hostsResult.hostsPath}`);
1266
1712
  }
1713
+ unregisterLocalghostSetup({ cwd: options.cwd, projectName });
1267
1714
  console.log(".localghost was left in place. Run localghost setup when you are ready.");
1268
1715
  });
1269
1716
  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 +1742,7 @@ program.command("teardown").description("Remove Localghost's managed /etc/hosts
1295
1742
  if (options.removeCaddyfile) {
1296
1743
  console.log(caddyfileRemoved ? `Removed ${caddyfilePath}` : `${caddyfilePath} was not present`);
1297
1744
  }
1745
+ unregisterLocalghostSetup({ cwd: options.cwd, projectName });
1298
1746
  console.log(`State ${statePath}`);
1299
1747
  });
1300
1748
  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 +1788,17 @@ program.command("status").description("Print Localghost's project-local state fi
1340
1788
  process.exitCode = 1;
1341
1789
  }
1342
1790
  });
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) => {
1791
+ 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
1792
  const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
1345
1793
  warnAboutLocalMdns(context.entries);
1346
1794
  console.log(formatDomainRoutes(context.entries, { https: options.http ? false : context.https }));
1795
+ if (context.ghostTunnel.enabled) {
1796
+ console.log(formatGhostTunnel(context.ghostTunnel, {
1797
+ color: shouldColor(),
1798
+ label: "expected",
1799
+ verbose: options.verbose === true
1800
+ }));
1801
+ }
1347
1802
  });
1348
1803
  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
1804
  assertLocalDevelopment("dev");
@@ -1385,9 +1840,17 @@ program.command("dev").description("Run the Localghost Caddy proxy after setup")
1385
1840
  ...existingTrustMarkers(options.cwd),
1386
1841
  entries: readiness.entries
1387
1842
  });
1843
+ registerLocalghostSetup({
1844
+ cwd: options.cwd,
1845
+ projectName: readiness.projectName,
1846
+ configPath: readiness.configPath,
1847
+ caddyfilePath,
1848
+ https,
1849
+ entries: readiness.entries
1850
+ });
1388
1851
  }
1389
1852
  warnAboutLocalMdns(readiness.entries);
1390
- logDomainRoutes(readiness.entries, { https });
1853
+ logDomainRoutes(readiness.entries, { https, ghostTunnel: context.ghostTunnel });
1391
1854
  const caddyfile = await writeCaddyfile(readiness.entries, options.cwd, { https });
1392
1855
  await validateCaddyfile(caddyfile);
1393
1856
  const caddy = startCaddy(caddyfile);
@@ -1420,7 +1883,7 @@ program.command("dev").description("Run the Localghost Caddy proxy after setup")
1420
1883
  cleanupRun();
1421
1884
  }
1422
1885
  });
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) => {
1886
+ program.command("run").description("Run Caddy and a dev command from the same Localghost context").option("--project <name>", "Managed /etc/hosts block name").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to look for. Can be repeated.", collect, []).option("--config-pattern <regex>", "Regex for config filenames in the project root").option("--port <number>", "Initial app port", parsePort2).option("--https", "Run a local HTTPS proxy with Caddy local certificates").option("--ssl", "Alias for --https").option("--setup", "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
1887
  assertLocalDevelopment("run");
1425
1888
  await assertCaddyReady();
1426
1889
  const context = await resolveLocalghostContext({
@@ -1459,7 +1922,7 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
1459
1922
  console.log(`Port ${context.requestedPort} is busy; using ${context.port}.`);
1460
1923
  }
1461
1924
  warnAboutLocalMdns(context.entries);
1462
- logDomainRoutes(context.entries, { https });
1925
+ logDomainRoutes(context.entries, { https, ghostTunnel: context.ghostTunnel });
1463
1926
  const caddyfile = await writeCaddyfile(context.entries, options.cwd, { https });
1464
1927
  await validateCaddyfile(caddyfile);
1465
1928
  const caddy = startCaddy(caddyfile);
@@ -1524,13 +1987,15 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
1524
1987
  cleanupRun();
1525
1988
  }
1526
1989
  });
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)));
1990
+ program.command("ps").description("Show Localghost setups and currently running sessions").option("--json", "Print raw JSON").action(async (options) => {
1991
+ const setups = listLocalghostSetups();
1992
+ const runs = listLocalghostRuns();
1993
+ const instances = await getInstanceViews(setups, runs);
1529
1994
  if (options.json) {
1530
- console.log(JSON.stringify({ activityPath: getLocalghostActivityPath(), runs }, null, 2));
1995
+ console.log(JSON.stringify({ activityPath: getLocalghostActivityPath(), setups, runs, instances }, null, 2));
1531
1996
  return;
1532
1997
  }
1533
- console.log(formatRunViews(runs));
1998
+ console.log(formatInstanceViews(instances));
1534
1999
  });
1535
2000
  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
2001
  const entries = readDevHosts(readOptionsFromCli(options));