@hamedb89/localghost 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -54,13 +54,13 @@ yarn dlx @hamedb89/localghost
54
54
  bunx --package @hamedb89/localghost localghost
55
55
  ```
56
56
 
57
- Test a real consumer repository against the local checkout without changing its manifest or working tree:
57
+ From a Localghost checkout, test a real consumer repository against the local checkout without changing the consumer's manifest or working tree. This is repository development tooling and is not part of the installed npm package:
58
58
 
59
59
  ```sh
60
60
  ./bin/ghost consumer test faaast --repo /path/to/faaast-landing -- pnpm run bench:noop
61
61
  ```
62
62
 
63
- The command creates a detached worktree, installs its dependencies, builds and links the local Localghost checkout, runs the command inside the worktree, and removes the worktree afterward. Add `--keep` before `--` when inspecting the isolated checkout after a failure.
63
+ The command creates a detached worktree, installs its dependencies, builds and links the local Localghost checkout, runs the command inside the worktree, and removes the worktree afterward. Add `--keep` before `--` when inspecting the isolated checkout after a failure. The FAAAST adapter uses a separate `.consumer.test` hostname suffix to avoid colliding with a normal `.localhost` development session.
64
64
 
65
65
  On the first interactive run, Localghost can create `.localghost`, explain the `/etc/hosts` change, write `ops/local/Caddyfile`, and print the browser-facing URL:
66
66
 
@@ -630,8 +630,8 @@ localghost ps [--json]
630
630
  localghost update [--json]
631
631
  localghost upgrade [--cwd path]
632
632
  localghost release [patch|minor|major]
633
- localghost dev [--config file] [--config-pattern regex] [--https|--ssl] [--auto-repair yes|no] [--trust]
634
- localghost run [--config file] [--config-pattern regex] [--https|--ssl] [--auto-repair yes|no] [--trust] [--dynamic-port] -- command
633
+ localghost dev [--config file] [--config-pattern regex] [--https|--ssl] [--auto-repair yes|no] [--clean-caddy] [--trust]
634
+ localghost run [--config file] [--config-pattern regex] [--https|--ssl] [--auto-repair yes|no] [--clean-caddy] [--trust] [--dynamic-port yes|no] -- command
635
635
  localghost routes [--https|--ssl]
636
636
  localghost print [--config file] [--config-pattern regex]
637
637
  ```
@@ -666,7 +666,7 @@ import { localGhostPlugin } from "@hamedb89/localghost/vite";
666
666
  - Preview the exact Pages artifact locally with `npm run site:serve`, then open `http://127.0.0.1:4173/`.
667
667
  - npm publish is guarded by `prepublishOnly` and the release workflow publishes with npm provenance.
668
668
  - To release the CLI, run `localghost release patch`, `localghost release minor`, or `localghost release major`. The command dispatches the **Release** workflow from `main`; it synchronizes version metadata, verifies the package and runtime matrix, commits and tags the bump, publishes npm, and creates a GitHub Release with generated notes. GitHub CLI must be installed and authenticated.
669
- - From this repository, `./bin/ghost release minor` (or the shorter `./bin/c release minor`) pushes `main` and dispatches the same guarded GitHub release workflow. Use `./bin/ghost check` for the release verification locally, `./bin/ghost release status` to inspect recent runs, or `./bin/ghost release retry v0.2.0` to retry publishing an existing tag.
669
+ - From this repository, `./bin/ghost release minor` (or the shorter `./bin/c release minor`) pushes `main` and dispatches the same guarded GitHub release workflow. Use `./bin/ghost check` for the release verification locally, `./bin/ghost release status` to inspect recent runs, or `./bin/ghost release retry v0.4.0` to retry publishing an existing tag.
670
670
  - Runtime dependencies are intentionally small: `commander` and `execa`. Vite is an optional peer dependency.
671
671
  - No postinstall scripts, hidden Homebrew installs, surprise browser tabs, or broad hosts-file rewrites.
672
672
  - Update checks are best-effort, cached for 24 hours, and can be disabled with `LOCALGHOST_NO_UPDATE_CHECK=1` or `--no-update-check`.
package/dist/cli.js CHANGED
@@ -487,13 +487,13 @@ import { pathToFileURL } from "url";
487
487
  // src/port.ts
488
488
  import { createServer } from "net";
489
489
  async function isPortAvailable(port, host = "127.0.0.1") {
490
- return new Promise((resolve4) => {
490
+ return new Promise((resolve5) => {
491
491
  const server = createServer();
492
492
  server.once("error", () => {
493
- resolve4(false);
493
+ resolve5(false);
494
494
  });
495
495
  server.once("listening", () => {
496
- server.close(() => resolve4(true));
496
+ server.close(() => resolve5(true));
497
497
  });
498
498
  server.listen(port, host);
499
499
  });
@@ -679,6 +679,18 @@ function createLocalghostRegistry(options = {}) {
679
679
  return lease;
680
680
  });
681
681
  },
682
+ async renewPort(renewOptions) {
683
+ const projectCwd = canonicalizeLocalghostProjectCwd(renewOptions.projectCwd ?? cwd);
684
+ return withLock(async (registry) => {
685
+ const lease = registry.leases.find(
686
+ (candidate) => candidate.projectCwd === projectCwd && candidate.instanceKey === renewOptions.instanceKey && candidate.ownerToken === ownerToken
687
+ );
688
+ if (!lease || lease.expiresAt <= now() || !isRunning(lease.pid)) return void 0;
689
+ lease.expiresAt = now() + (renewOptions.leaseTtlMs ?? 30 * 60 * 1e3);
690
+ await writeRegistry(registry);
691
+ return lease;
692
+ });
693
+ },
682
694
  async releasePort(releaseOptions) {
683
695
  const projectCwd = canonicalizeLocalghostProjectCwd(releaseOptions.projectCwd ?? cwd);
684
696
  return withLock(async (registry) => {
@@ -1558,11 +1570,11 @@ function isStopped(signal, localSignal) {
1558
1570
  }
1559
1571
  function wait(ms, signal, localSignal) {
1560
1572
  if (isStopped(signal, localSignal)) return Promise.resolve();
1561
- return new Promise((resolve4) => {
1562
- const timeout = setTimeout(resolve4, ms);
1573
+ return new Promise((resolve5) => {
1574
+ const timeout = setTimeout(resolve5, ms);
1563
1575
  const stop = () => {
1564
1576
  clearTimeout(timeout);
1565
- resolve4();
1577
+ resolve5();
1566
1578
  };
1567
1579
  signal?.addEventListener("abort", stop, { once: true });
1568
1580
  localSignal.addEventListener("abort", stop, { once: true });
@@ -1784,13 +1796,23 @@ async function removeSystemHosts(projectName) {
1784
1796
 
1785
1797
  // src/init.ts
1786
1798
  import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
1787
- import { join as join8 } from "path";
1799
+ import { dirname as dirname4, join as join8, resolve as resolve4 } from "path";
1788
1800
  function detectPackageManager(cwd = process.cwd()) {
1789
1801
  if (existsSync5(join8(cwd, "pnpm-lock.yaml"))) return "pnpm";
1790
1802
  if (existsSync5(join8(cwd, "yarn.lock"))) return "yarn";
1791
1803
  if (existsSync5(join8(cwd, "bun.lock")) || existsSync5(join8(cwd, "bun.lockb"))) return "bun";
1792
1804
  return "npm";
1793
1805
  }
1806
+ function isPnpmWorkspaceRoot(cwd = process.cwd()) {
1807
+ const target = resolve4(cwd);
1808
+ let current = target;
1809
+ while (true) {
1810
+ if (existsSync5(join8(current, "pnpm-workspace.yaml"))) return current === target;
1811
+ const parent = dirname4(current);
1812
+ if (parent === current) return false;
1813
+ current = parent;
1814
+ }
1815
+ }
1794
1816
  function packageRunCommand(packageManager, script) {
1795
1817
  if (packageManager === "yarn") return `yarn ${script}`;
1796
1818
  if (packageManager === "pnpm") return `pnpm ${script}`;
@@ -1910,6 +1932,59 @@ function initLocalghost(options = {}) {
1910
1932
  };
1911
1933
  }
1912
1934
 
1935
+ // src/test-session.ts
1936
+ async function createLocalghostTestSession(options) {
1937
+ if (!options.instanceKey) throw new Error("instanceKey is required");
1938
+ const registry = createLocalghostRegistry(options.cwd ? { cwd: options.cwd } : {});
1939
+ const leases = [];
1940
+ const ports = {};
1941
+ const leaseTtlMs = options.leaseTtlMs ?? 30 * 60 * 1e3;
1942
+ if (!Number.isFinite(leaseTtlMs) || leaseTtlMs < 1e3) throw new Error("leaseTtlMs must be at least 1000 milliseconds.");
1943
+ try {
1944
+ for (const [name, service] of Object.entries(options.services)) {
1945
+ const lease = await registry.acquirePort({
1946
+ ...options.cwd ? { projectCwd: options.cwd } : {},
1947
+ instanceKey: `test:${options.instanceKey}:${name}`,
1948
+ startPort: service.startPort,
1949
+ ...service.maxAttempts !== void 0 ? { maxAttempts: service.maxAttempts } : {},
1950
+ ...service.host !== void 0 ? { host: service.host } : {},
1951
+ leaseTtlMs,
1952
+ reservedPorts: Object.values(ports)
1953
+ });
1954
+ leases.push(lease);
1955
+ ports[name] = lease.port;
1956
+ }
1957
+ } catch (error) {
1958
+ await Promise.all(leases.map((lease) => registry.releasePort({ projectCwd: lease.projectCwd, instanceKey: lease.instanceKey })));
1959
+ throw error;
1960
+ }
1961
+ let released = false;
1962
+ const renew = async () => {
1963
+ if (released) return;
1964
+ await Promise.all(leases.map(async (lease) => {
1965
+ const renewed = await registry.renewPort({ projectCwd: lease.projectCwd, instanceKey: lease.instanceKey, leaseTtlMs });
1966
+ if (!renewed) throw new Error(`Localghost test lease expired: ${lease.instanceKey}`);
1967
+ }));
1968
+ };
1969
+ return {
1970
+ instanceKey: options.instanceKey,
1971
+ ports,
1972
+ leases,
1973
+ renew,
1974
+ startHeartbeat(intervalMs = Math.max(1e3, Math.floor(leaseTtlMs / 3))) {
1975
+ if (!Number.isFinite(intervalMs) || intervalMs < 1e3) throw new Error("Heartbeat interval must be at least 1000 milliseconds.");
1976
+ const timer = setInterval(() => void renew().catch(() => void 0), intervalMs);
1977
+ timer.unref();
1978
+ return timer;
1979
+ },
1980
+ async release() {
1981
+ if (released) return;
1982
+ released = true;
1983
+ await Promise.all(leases.map((lease) => registry.releasePort({ projectCwd: lease.projectCwd, instanceKey: lease.instanceKey })));
1984
+ }
1985
+ };
1986
+ }
1987
+
1913
1988
  // src/guide.ts
1914
1989
  var LOCALGHOST_AGENT_GUIDE = `# Localghost agent guide
1915
1990
 
@@ -2100,9 +2175,9 @@ function patchLocalghostState(cwd, patch) {
2100
2175
  // src/update-check.ts
2101
2176
  import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
2102
2177
  import { homedir as homedir3 } from "os";
2103
- import { dirname as dirname4, join as join10 } from "path";
2178
+ import { dirname as dirname5, join as join10 } from "path";
2104
2179
  var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
2105
- var LOCALGHOST_VERSION = "0.4.0";
2180
+ var LOCALGHOST_VERSION = "0.5.0";
2106
2181
  var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
2107
2182
  var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
2108
2183
  var UPDATE_CHECK_TIMEOUT_MS = 900;
@@ -2127,7 +2202,7 @@ function readCache(path = getUpdateCheckCachePath()) {
2127
2202
  }
2128
2203
  function writeCache(cache, path = getUpdateCheckCachePath()) {
2129
2204
  try {
2130
- mkdirSync3(dirname4(path), { recursive: true });
2205
+ mkdirSync3(dirname5(path), { recursive: true });
2131
2206
  writeFileSync5(path, `${JSON.stringify(cache, null, 2)}
2132
2207
  `, "utf8");
2133
2208
  } catch {
@@ -2320,6 +2395,13 @@ function parsePort2(value) {
2320
2395
  }
2321
2396
  return port;
2322
2397
  }
2398
+ function parseCount(value) {
2399
+ const count = Number.parseInt(value, 10);
2400
+ if (!Number.isInteger(count) || count < 1 || count > 100) {
2401
+ throw new InvalidArgumentError("Count must be a number between 1 and 100.");
2402
+ }
2403
+ return count;
2404
+ }
2323
2405
  function parsePackageManager(value) {
2324
2406
  if (value === "npm" || value === "yarn" || value === "pnpm" || value === "bun") return value;
2325
2407
  throw new InvalidArgumentError("Package manager must be npm, pnpm, yarn, or bun.");
@@ -2496,7 +2578,7 @@ async function runSetupFromReadiness(cwd, https, readiness) {
2496
2578
  });
2497
2579
  }
2498
2580
  function wait2(ms) {
2499
- return new Promise((resolve4) => setTimeout(resolve4, ms));
2581
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
2500
2582
  }
2501
2583
  async function runTrust(cwd, caddyfilePath) {
2502
2584
  await wait2(350);
@@ -2527,6 +2609,69 @@ async function maybeTrustCaddy(options) {
2527
2609
  }
2528
2610
  await runTrust(options.cwd, options.caddyfilePath);
2529
2611
  }
2612
+ async function readPortStatuses() {
2613
+ const registry = createLocalghostRegistry();
2614
+ const data = await registry.read();
2615
+ const rows = /* @__PURE__ */ new Map();
2616
+ const now = Date.now();
2617
+ for (const allocation of data.allocations) {
2618
+ rows.set(`${allocation.projectCwd}\0${allocation.instanceKey}`, {
2619
+ port: allocation.port,
2620
+ state: "available",
2621
+ projectCwd: allocation.projectCwd,
2622
+ instanceKey: allocation.instanceKey,
2623
+ available: true
2624
+ });
2625
+ }
2626
+ for (const lease of data.leases) {
2627
+ const running = lease.expiresAt > now && isProcessRunning(lease.pid);
2628
+ const key = `${lease.projectCwd}\0${lease.instanceKey}`;
2629
+ rows.set(key, {
2630
+ port: lease.port,
2631
+ state: running ? "active" : "stale",
2632
+ projectCwd: lease.projectCwd,
2633
+ instanceKey: lease.instanceKey,
2634
+ available: false,
2635
+ pid: lease.pid,
2636
+ processRunning: isProcessRunning(lease.pid),
2637
+ expiresAt: lease.expiresAt
2638
+ });
2639
+ }
2640
+ const activity = readLocalghostActivity();
2641
+ for (const run of activity.runs) {
2642
+ const ports = [...new Set(run.entries.map((entry) => entry.port))];
2643
+ for (const port of ports) {
2644
+ const key = `${run.cwd}\0activity:${run.id}:${port}`;
2645
+ if (!rows.has(key)) {
2646
+ const running = isProcessRunning(run.pid);
2647
+ rows.set(key, {
2648
+ port,
2649
+ state: running ? "active" : "stale",
2650
+ projectCwd: run.cwd,
2651
+ instanceKey: `activity:${run.mode}`,
2652
+ available: false,
2653
+ pid: run.pid,
2654
+ processRunning: running
2655
+ });
2656
+ }
2657
+ }
2658
+ }
2659
+ const statuses = await Promise.all([...rows.values()].map(async (row) => {
2660
+ if (row.state === "active" || row.state === "stale") {
2661
+ return { ...row, available: await isPortAvailable(row.port) };
2662
+ }
2663
+ const available = await isPortAvailable(row.port);
2664
+ return { ...row, available, state: available ? "available" : "occupied" };
2665
+ }));
2666
+ return { registryPath: registry.registryPath, statuses };
2667
+ }
2668
+ async function findFreePorts(startPort, count) {
2669
+ const ports = [];
2670
+ for (let port = startPort; port <= 65535 && ports.length < count; port += 1) {
2671
+ if (await isPortAvailable(port)) ports.push(port);
2672
+ }
2673
+ return ports;
2674
+ }
2530
2675
  function maybePid(pid) {
2531
2676
  return typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? pid : void 0;
2532
2677
  }
@@ -2908,6 +3053,9 @@ program.command("upgrade").description("Install or update Localghost in the curr
2908
3053
  const packageManager = detectPackageManager(options.cwd);
2909
3054
  const packageName = "@hamedb89/localghost@latest";
2910
3055
  const args = packageManager === "yarn" ? ["add", "--dev", packageName] : packageManager === "pnpm" ? ["add", "--save-dev", packageName] : packageManager === "bun" ? ["add", "--dev", packageName] : ["install", "--save-dev", packageName];
3056
+ if (packageManager === "pnpm" && isPnpmWorkspaceRoot(options.cwd)) {
3057
+ args.splice(1, 0, "--workspace-root");
3058
+ }
2911
3059
  console.log(`Upgrading ${packageName} with ${packageManager}...`);
2912
3060
  await execa4(packageManager, args, { cwd: options.cwd, stdio: "inherit" });
2913
3061
  console.log(`Localghost is upgraded in ${options.cwd}.`);
@@ -3097,7 +3245,68 @@ program.command("teardown").description("Remove Localghost's managed /etc/hosts
3097
3245
  unregisterLocalghostSetup({ cwd: options.cwd, projectName });
3098
3246
  console.log(`State ${statePath}`);
3099
3247
  });
3100
- 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) => {
3248
+ program.command("test").description("Run a test command with an isolated Localghost port").option("--cwd <path>", "Project directory", process.cwd()).option("--instance <name>", "Parallel test instance name", String(process.pid)).option("--port <number>", "Initial test port", parsePort2, 5173).option("--lease-ttl <milliseconds>", "Lease lifetime before heartbeat renewal", Number, 30 * 60 * 1e3).argument("<command...>", "Command to run after --").action(async (command, options) => {
3249
+ const [binary, ...args] = command;
3250
+ if (!binary) throw new Error("Missing command. Use: localghost test -- <command>");
3251
+ const session = await createLocalghostTestSession({
3252
+ cwd: options.cwd,
3253
+ instanceKey: options.instance,
3254
+ leaseTtlMs: options.leaseTtl,
3255
+ services: { default: { startPort: options.port } }
3256
+ });
3257
+ const heartbeat = session.startHeartbeat();
3258
+ const child = execa4(binary, args, {
3259
+ cwd: options.cwd,
3260
+ stdio: "inherit",
3261
+ detached: process.platform !== "win32",
3262
+ env: {
3263
+ ...process.env,
3264
+ LOCALGHOST_INSTANCE: options.instance,
3265
+ LOCALGHOST_PORT: String(session.ports.default),
3266
+ LOCALGHOST_DYNAMIC_PORT: "1",
3267
+ VITE_PORT: String(session.ports.default)
3268
+ }
3269
+ });
3270
+ const stop = (signal) => signalManagedProcess(child, signal);
3271
+ process.once("SIGINT", stop);
3272
+ process.once("SIGTERM", stop);
3273
+ try {
3274
+ await child;
3275
+ } finally {
3276
+ process.off("SIGINT", stop);
3277
+ process.off("SIGTERM", stop);
3278
+ clearInterval(heartbeat);
3279
+ await session.release();
3280
+ }
3281
+ });
3282
+ 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("--ports", "List Localghost-managed and active ports").option("--from <port>", "First port to probe for free ports", parsePort2, 3e3).option("--count <number>", "Number of free ports to find", parseCount, 5).option("--json", "Print raw JSON").action(async (options) => {
3283
+ if (options.ports) {
3284
+ const [{ registryPath, statuses }, freePorts] = await Promise.all([
3285
+ readPortStatuses(),
3286
+ findFreePorts(options.from, options.count)
3287
+ ]);
3288
+ const result = {
3289
+ registryPath,
3290
+ ports: statuses.sort((left, right) => left.port - right.port),
3291
+ free: { from: options.from, count: options.count, ports: freePorts }
3292
+ };
3293
+ if (options.json) {
3294
+ console.log(JSON.stringify(result, null, 2));
3295
+ return;
3296
+ }
3297
+ console.log(`Registry: ${registryPath}`);
3298
+ if (result.ports.length === 0) {
3299
+ console.log("No Localghost ports are currently recorded.");
3300
+ } else {
3301
+ console.log("Port State Project / instance");
3302
+ for (const port of result.ports) {
3303
+ const owner = `${port.projectCwd} / ${port.instanceKey}`;
3304
+ console.log(`${String(port.port).padEnd(5)} ${port.state.padEnd(10)} ${owner}`);
3305
+ }
3306
+ }
3307
+ console.log(`Free ports from ${options.from}: ${freePorts.join(", ") || "none found"}`);
3308
+ return;
3309
+ }
3101
3310
  const state = readLocalghostState(options.cwd);
3102
3311
  const statePath = getLocalghostStatePath(options.cwd);
3103
3312
  const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
@@ -3230,107 +3439,110 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
3230
3439
  ...typeof options.dynamicPort === "boolean" ? { dynamicPort: options.dynamicPort } : {},
3231
3440
  ...typeof options.autoRepair === "boolean" ? { autoRepair: options.autoRepair } : {}
3232
3441
  });
3233
- const https = context.https;
3234
- const readiness = getSetupReadiness({
3235
- ...options,
3236
- https,
3237
- ignoreCaddyfile: true,
3238
- entries: context.entries,
3239
- configPath: context.configPath,
3240
- projectName: context.projectName
3241
- });
3242
- if (!readiness.ready) {
3243
- if (!options.setup && !context.autoRepair) {
3244
- throw new Error(
3245
- [
3246
- "Localghost setup is missing or stale.",
3247
- ...readiness.reasons.map((reason) => `- ${reason}`),
3248
- `Run: ${readiness.setupCommand}`,
3249
- "Automatic repair is disabled. Rerun without --auto-repair=no or run localghost repair."
3250
- ].join("\n")
3251
- );
3252
- }
3253
- console.log("Localghost setup is stale; repairing it now.");
3254
- await runSetupFromReadiness(options.cwd, https, readiness);
3255
- console.log(`Repair complete. Setup state: ${getLocalghostStatePath(options.cwd)}`);
3256
- }
3257
- if (context.dynamicPort && context.port !== context.requestedPort) {
3258
- console.log(`Port ${context.requestedPort} is busy; using ${context.port}.`);
3259
- }
3260
- warnAboutLocalMdns(context.entries);
3261
- logDomainRoutes(context.entries, { https, ghostTunnel: context.ghostTunnel });
3262
- const caddyfile = await writeCaddyfile(context.entries, options.cwd, { https });
3263
- await validateCaddyfile(caddyfile);
3264
- const caddy = startCaddy(caddyfile);
3265
- const caddyExit = caddy.catch((error) => {
3266
- if (!caddy.killed) throw error;
3267
- });
3268
3442
  try {
3269
- await maybeTrustCaddy({
3270
- cwd: options.cwd,
3443
+ const https = context.https;
3444
+ const readiness = getSetupReadiness({
3445
+ ...options,
3271
3446
  https,
3272
- caddyfilePath: caddyfile,
3273
- ...typeof options.trust === "boolean" ? { trust: options.trust } : {}
3447
+ ignoreCaddyfile: true,
3448
+ entries: context.entries,
3449
+ configPath: context.configPath,
3450
+ projectName: context.projectName
3274
3451
  });
3275
- } catch (error) {
3276
- signalManagedProcess(caddy, "SIGINT");
3277
- throw error;
3278
- }
3279
- const [binary, ...args] = command;
3280
- if (!binary) {
3281
- throw new Error("Missing command. Use: localghost run -- vite");
3282
- }
3283
- const child = execa4(binary, args, {
3284
- cwd: options.cwd,
3285
- stdio: "inherit",
3286
- detached: process.platform !== "win32",
3287
- env: {
3288
- ...process.env,
3289
- LOCALGHOST_PORT: String(context.port),
3290
- LOCALGHOST_DYNAMIC_PORT: context.dynamicPort ? "1" : "0",
3291
- VITE_PORT: String(context.port)
3452
+ if (!readiness.ready) {
3453
+ if (!options.setup && !context.autoRepair) {
3454
+ throw new Error(
3455
+ [
3456
+ "Localghost setup is missing or stale.",
3457
+ ...readiness.reasons.map((reason) => `- ${reason}`),
3458
+ `Run: ${readiness.setupCommand}`,
3459
+ "Automatic repair is disabled. Rerun without --auto-repair=no or run localghost repair."
3460
+ ].join("\n")
3461
+ );
3462
+ }
3463
+ console.log("Localghost setup is stale; repairing it now.");
3464
+ await runSetupFromReadiness(options.cwd, https, readiness);
3465
+ console.log(`Repair complete. Setup state: ${getLocalghostStatePath(options.cwd)}`);
3292
3466
  }
3293
- });
3294
- const caddyPid = maybePid(caddy.pid);
3295
- const childPid = maybePid(child.pid);
3296
- const runRecord = registerLocalghostRun({
3297
- mode: "run",
3298
- cwd: context.cwd,
3299
- projectName: context.projectName,
3300
- configPath: context.configPath,
3301
- caddyfilePath: caddyfile,
3302
- ...caddyPid ? { caddyPid } : {},
3303
- ...caddyPid ? { caddyPgid: caddyPid } : {},
3304
- ...childPid ? { childPid } : {},
3305
- childCommand: command,
3306
- https,
3307
- requestedPort: context.requestedPort,
3308
- port: context.port,
3309
- dynamicPort: context.dynamicPort,
3310
- entries: context.entries
3311
- });
3312
- const cleanupRun = registerCleanup(runRecord.id);
3313
- const stopManaged = (signal) => {
3314
- signalManagedProcess(child, signal);
3315
- signalManagedProcess(caddy, signal);
3316
- };
3317
- const finishShutdown = registerSignalShutdown(() => stopManaged("SIGINT"));
3318
- try {
3319
- await Promise.race([child, caddyExit]);
3320
- } finally {
3321
- finishShutdown();
3322
- await waitForProcessShutdown(
3323
- [child, caddyExit],
3324
- stopManaged
3325
- );
3326
- if (!await waitForPortsToBeAvailable(context.entries)) {
3327
- console.warn("Localghost: timed out waiting for service ports to be released.");
3328
- stopManaged("SIGTERM");
3329
- if (!await waitForPortsToBeAvailable(context.entries, 2e3)) {
3330
- stopManaged("SIGKILL");
3467
+ if (context.dynamicPort && context.port !== context.requestedPort) {
3468
+ console.log(`Port ${context.requestedPort} is busy; using ${context.port}.`);
3469
+ }
3470
+ warnAboutLocalMdns(context.entries);
3471
+ logDomainRoutes(context.entries, { https, ghostTunnel: context.ghostTunnel });
3472
+ const caddyfile = await writeCaddyfile(context.entries, options.cwd, { https });
3473
+ await validateCaddyfile(caddyfile);
3474
+ const caddy = startCaddy(caddyfile);
3475
+ const caddyExit = caddy.catch((error) => {
3476
+ if (!caddy.killed) throw error;
3477
+ });
3478
+ try {
3479
+ await maybeTrustCaddy({
3480
+ cwd: options.cwd,
3481
+ https,
3482
+ caddyfilePath: caddyfile,
3483
+ ...typeof options.trust === "boolean" ? { trust: options.trust } : {}
3484
+ });
3485
+ } catch (error) {
3486
+ signalManagedProcess(caddy, "SIGINT");
3487
+ throw error;
3488
+ }
3489
+ const [binary, ...args] = command;
3490
+ if (!binary) {
3491
+ throw new Error("Missing command. Use: localghost run -- vite");
3492
+ }
3493
+ const child = execa4(binary, args, {
3494
+ cwd: options.cwd,
3495
+ stdio: "inherit",
3496
+ detached: process.platform !== "win32",
3497
+ env: {
3498
+ ...process.env,
3499
+ LOCALGHOST_PORT: String(context.port),
3500
+ LOCALGHOST_DYNAMIC_PORT: context.dynamicPort ? "1" : "0",
3501
+ VITE_PORT: String(context.port)
3331
3502
  }
3503
+ });
3504
+ const caddyPid = maybePid(caddy.pid);
3505
+ const childPid = maybePid(child.pid);
3506
+ const runRecord = registerLocalghostRun({
3507
+ mode: "run",
3508
+ cwd: context.cwd,
3509
+ projectName: context.projectName,
3510
+ configPath: context.configPath,
3511
+ caddyfilePath: caddyfile,
3512
+ ...caddyPid ? { caddyPid } : {},
3513
+ ...caddyPid ? { caddyPgid: caddyPid } : {},
3514
+ ...childPid ? { childPid } : {},
3515
+ childCommand: command,
3516
+ https,
3517
+ requestedPort: context.requestedPort,
3518
+ port: context.port,
3519
+ dynamicPort: context.dynamicPort,
3520
+ entries: context.entries
3521
+ });
3522
+ const cleanupRun = registerCleanup(runRecord.id);
3523
+ const stopManaged = (signal) => {
3524
+ signalManagedProcess(child, signal);
3525
+ signalManagedProcess(caddy, signal);
3526
+ };
3527
+ const finishShutdown = registerSignalShutdown(() => stopManaged("SIGINT"));
3528
+ try {
3529
+ await Promise.race([child, caddyExit]);
3530
+ } finally {
3531
+ finishShutdown();
3532
+ await waitForProcessShutdown(
3533
+ [child, caddyExit],
3534
+ stopManaged
3535
+ );
3536
+ if (!await waitForPortsToBeAvailable(context.entries)) {
3537
+ console.warn("Localghost: timed out waiting for service ports to be released.");
3538
+ stopManaged("SIGTERM");
3539
+ if (!await waitForPortsToBeAvailable(context.entries, 2e3)) {
3540
+ stopManaged("SIGKILL");
3541
+ }
3542
+ }
3543
+ cleanupRun();
3332
3544
  }
3333
- cleanupRun();
3545
+ } finally {
3334
3546
  await context.releasePort?.();
3335
3547
  }
3336
3548
  });