@hamedb89/localghost 0.1.6 → 0.1.8

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 existsSync6, readFileSync as readFileSync6, unlinkSync } from "fs";
4
+ import { existsSync as existsSync7, readFileSync as readFileSync6, unlinkSync } from "fs";
5
5
  import { Command, InvalidArgumentError } from "commander";
6
6
 
7
7
  // src/activity.ts
@@ -280,6 +280,16 @@ function startCaddy(path) {
280
280
  stdio: "inherit"
281
281
  });
282
282
  }
283
+ async function trustCaddy(path) {
284
+ await execa("caddy", ["trust", "--config", path], {
285
+ cwd: dirname3(path),
286
+ stdio: "inherit"
287
+ });
288
+ }
289
+
290
+ // src/context.ts
291
+ import { existsSync as existsSync3 } from "fs";
292
+ import { pathToFileURL } from "url";
283
293
 
284
294
  // src/port.ts
285
295
  import { createServer } from "net";
@@ -308,6 +318,11 @@ async function findAvailablePort(startPort, options = {}) {
308
318
  }
309
319
 
310
320
  // src/context.ts
321
+ var LOCALGHOST_PROJECT_CONFIG_FILES = [
322
+ "localghost.config.mjs",
323
+ "localghost.config.js",
324
+ "localghost.config.cjs"
325
+ ];
311
326
  function parsePort(value) {
312
327
  if (!value) return void 0;
313
328
  const port = Number.parseInt(value, 10);
@@ -321,6 +336,11 @@ function envDynamicPort() {
321
336
  if (!value) return void 0;
322
337
  return ["1", "true", "yes", "on"].includes(value.toLowerCase());
323
338
  }
339
+ function envHttps() {
340
+ const value = process.env.LOCALGHOST_HTTPS;
341
+ if (!value) return void 0;
342
+ return ["1", "true", "yes", "on"].includes(value.toLowerCase());
343
+ }
324
344
  function readOptionsFromContext(options) {
325
345
  return {
326
346
  cwd: options.cwd ?? process.cwd(),
@@ -338,22 +358,58 @@ function withRuntimePort(entries, requestedPort, port) {
338
358
  function uniqueHosts(entries) {
339
359
  return [...new Set(entries.map((entry) => entry.host))];
340
360
  }
361
+ function isAliasableHost(host) {
362
+ return host.includes(".") && !host.startsWith("www.") && !host.includes(":");
363
+ }
364
+ function getDefaultWwwAlias(host) {
365
+ return isAliasableHost(host) ? `www.${host}` : null;
366
+ }
367
+ function addDefaultWwwAliases(entries) {
368
+ const seen = new Set(entries.map((entry) => entry.host));
369
+ const aliases = [];
370
+ for (const entry of entries) {
371
+ const alias = getDefaultWwwAlias(entry.host);
372
+ if (alias && !seen.has(alias)) {
373
+ aliases.push({ host: alias, port: entry.port, target: `127.0.0.1:${entry.port}` });
374
+ seen.add(alias);
375
+ }
376
+ }
377
+ return [...entries, ...aliases];
378
+ }
379
+ function defined(input2) {
380
+ return Object.fromEntries(Object.entries(input2).filter(([, value]) => typeof value !== "undefined"));
381
+ }
382
+ async function readProjectConfig(cwd, configFile) {
383
+ if (configFile === false) return {};
384
+ const candidates = configFile ? [configFile] : LOCALGHOST_PROJECT_CONFIG_FILES;
385
+ const path = candidates.map((candidate) => resolveDevHostsPath({ cwd, fileName: candidate }).path).find((candidate) => existsSync3(candidate));
386
+ if (!path) return {};
387
+ const imported = await import(`${pathToFileURL(path).href}?localghost=${Date.now()}`);
388
+ const config = imported.default ?? imported;
389
+ return { config, path };
390
+ }
341
391
  async function resolveLocalghostContext(options = {}) {
342
392
  const cwd = options.cwd ?? process.cwd();
343
- const readOptions = readOptionsFromContext({ ...options, cwd });
393
+ const projectConfig = await readProjectConfig(cwd, options.localghostConfig);
394
+ const merged = {
395
+ ...projectConfig.config,
396
+ ...defined(options)
397
+ };
398
+ const readOptions = readOptionsFromContext({ ...merged, cwd });
344
399
  const resolvedPath = resolveDevHostsPath(readOptions);
345
400
  const configEntries = readDevHosts(readOptions);
346
- const requestedPort = options.port ?? envPort() ?? configEntries[0]?.port ?? 5173;
347
- const dynamicPort = options.dynamicPort ?? envDynamicPort() ?? false;
348
- const bindHost = options.bindHost ?? "127.0.0.1";
401
+ const requestedPort = merged.port ?? envPort() ?? configEntries[0]?.port ?? 5173;
402
+ const dynamicPort = merged.dynamicPort ?? envDynamicPort() ?? false;
403
+ const bindHost = merged.bindHost ?? "127.0.0.1";
349
404
  const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
350
405
  const port = dynamicPort ? await findAvailablePort(requestedPort, { host: probeHost }) : requestedPort;
351
- const entries = withRuntimePort(configEntries, requestedPort, port);
406
+ const wwwAlias = merged.wwwAlias ?? true;
407
+ const entries = wwwAlias ? addDefaultWwwAliases(withRuntimePort(configEntries, requestedPort, port)) : withRuntimePort(configEntries, requestedPort, port);
352
408
  const hosts = uniqueHosts(entries);
353
- const primaryHost = options.primaryHost ?? entries.find((entry) => entry.port === port)?.host ?? hosts[0] ?? `${sanitizeProjectName(getProjectName(cwd))}.localhost`;
409
+ const primaryHost = merged.primaryHost ?? entries.find((entry) => entry.port === port)?.host ?? hosts[0] ?? `${sanitizeProjectName(getProjectName(cwd))}.localhost`;
354
410
  return {
355
411
  cwd,
356
- projectName: sanitizeProjectName(options.project ?? getProjectName(cwd)),
412
+ projectName: sanitizeProjectName(merged.project ?? getProjectName(cwd)),
357
413
  readOptions,
358
414
  configPath: resolvedPath.path,
359
415
  configFileName: resolvedPath.fileName,
@@ -365,7 +421,9 @@ async function resolveLocalghostContext(options = {}) {
365
421
  dynamicPort,
366
422
  bindHost,
367
423
  primaryHost,
368
- https: options.https === true
424
+ https: merged.https ?? envHttps() ?? false,
425
+ wwwAlias,
426
+ ...projectConfig.path ? { projectConfigPath: projectConfig.path } : {}
369
427
  };
370
428
  }
371
429
 
@@ -490,11 +548,11 @@ async function removeSystemHosts(projectName) {
490
548
  }
491
549
 
492
550
  // src/init.ts
493
- import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
551
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
494
552
  import { join as join5 } from "path";
495
553
  function detectPackageManager(cwd = process.cwd()) {
496
- if (existsSync3(join5(cwd, "pnpm-lock.yaml"))) return "pnpm";
497
- if (existsSync3(join5(cwd, "yarn.lock"))) return "yarn";
554
+ if (existsSync4(join5(cwd, "pnpm-lock.yaml"))) return "pnpm";
555
+ if (existsSync4(join5(cwd, "yarn.lock"))) return "yarn";
498
556
  return "npm";
499
557
  }
500
558
  function packageRunCommand(packageManager, script) {
@@ -538,6 +596,7 @@ function updatePackageScripts(packageJsonPath, configFile) {
538
596
  "localghost:proxy:https": scripts["localghost:proxy:https"] ?? `localghost dev${configFlag} --https`,
539
597
  "localghost:run": scripts["localghost:run"] ?? `localghost run${configFlag} --`,
540
598
  "localghost:ready": scripts["localghost:ready"] ?? `localghost status${configFlag} --ready`,
599
+ "localghost:trust": scripts["localghost:trust"] ?? `localghost trust${configFlag}`,
541
600
  "localghost:ps": scripts["localghost:ps"] ?? "localghost ps",
542
601
  "localghost:print": scripts["localghost:print"] ?? `localghost print${configFlag}`,
543
602
  "localghost:routes": scripts["localghost:routes"] ?? `localghost routes${configFlag}`,
@@ -566,7 +625,7 @@ function initLocalghost(options = {}) {
566
625
  const packageManager = options.packageManager ?? detectPackageManager(cwd);
567
626
  const configFile = options.configFile ?? LOCALGHOST_CONFIG_FILE;
568
627
  const configPath = join5(cwd, configFile);
569
- const configExists = existsSync3(configPath);
628
+ const configExists = existsSync4(configPath);
570
629
  if (configExists && !options.force) {
571
630
  return {
572
631
  configPath,
@@ -587,7 +646,7 @@ function initLocalghost(options = {}) {
587
646
  return {
588
647
  configPath,
589
648
  configCreated: true,
590
- ...existsSync3(packageJsonPath) ? { packageJsonPath } : {},
649
+ ...existsSync4(packageJsonPath) ? { packageJsonPath } : {},
591
650
  packageJsonChanged,
592
651
  packageManager,
593
652
  nextSteps: [
@@ -644,7 +703,7 @@ function formatDomainRoutes(entries, options = {}) {
644
703
  }
645
704
 
646
705
  // src/state.ts
647
- import { existsSync as existsSync4 } from "fs";
706
+ import { existsSync as existsSync5 } from "fs";
648
707
  import { join as join6 } from "path";
649
708
  var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
650
709
  function getLocalghostStatePath(cwd = process.cwd()) {
@@ -652,22 +711,27 @@ function getLocalghostStatePath(cwd = process.cwd()) {
652
711
  }
653
712
  function readLocalghostState(cwd = process.cwd()) {
654
713
  const path = getLocalghostStatePath(cwd);
655
- if (!existsSync4(path)) return null;
714
+ if (!existsSync5(path)) return null;
656
715
  return JSON.parse(readTextFile(path));
657
716
  }
658
717
  function writeLocalghostState(cwd, state) {
659
718
  const path = getLocalghostStatePath(cwd);
660
- writeTextFile(path, `${JSON.stringify({ version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), ...state }, null, 2)}
719
+ writeTextFile(path, `${JSON.stringify({ ...state, version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
661
720
  `);
662
721
  return path;
663
722
  }
723
+ function patchLocalghostState(cwd, patch) {
724
+ const current = readLocalghostState(cwd);
725
+ if (!current) return null;
726
+ return writeLocalghostState(cwd, { ...current, ...patch });
727
+ }
664
728
 
665
729
  // src/update-check.ts
666
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
730
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
667
731
  import { homedir as homedir2 } from "os";
668
732
  import { dirname as dirname4, join as join7 } from "path";
669
733
  var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
670
- var LOCALGHOST_VERSION = "0.1.6";
734
+ var LOCALGHOST_VERSION = "0.1.8";
671
735
  var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
672
736
  var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
673
737
  var UPDATE_CHECK_TIMEOUT_MS = 900;
@@ -683,7 +747,7 @@ function getUpdateCheckCachePath(env = process.env) {
683
747
  return join7(cacheRoot, "localghost", "update-check.json");
684
748
  }
685
749
  function readCache(path = getUpdateCheckCachePath()) {
686
- if (!existsSync5(path)) return null;
750
+ if (!existsSync6(path)) return null;
687
751
  try {
688
752
  return JSON.parse(readFileSync5(path, "utf8"));
689
753
  } catch {
@@ -864,6 +928,15 @@ function parseBooleanLike(value) {
864
928
  if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
865
929
  throw new InvalidArgumentError("Value must be yes or no.");
866
930
  }
931
+ function contextOptionsFromCli(options) {
932
+ return {
933
+ cwd: options.cwd,
934
+ ...options.project ? { project: options.project } : {},
935
+ ...options.config && options.config.length > 0 ? { configFiles: options.config } : {},
936
+ ...options.configPattern ? { configPattern: options.configPattern } : {},
937
+ ...useHttps(options) ? { https: true } : {}
938
+ };
939
+ }
867
940
  function readOptionsFromCli(options) {
868
941
  return {
869
942
  cwd: options.cwd,
@@ -880,10 +953,22 @@ async function assertCaddyReady() {
880
953
  "Localghost will not install it for you. No surprise spells."
881
954
  ].join("\n"));
882
955
  }
956
+ function existingTrustMarkers(cwd) {
957
+ const state = readLocalghostState(cwd);
958
+ return {
959
+ ...state?.caddyTrustedAt ? { caddyTrustedAt: state.caddyTrustedAt } : {},
960
+ ...state?.caddyTrustPromptedAt ? { caddyTrustPromptedAt: state.caddyTrustPromptedAt } : {}
961
+ };
962
+ }
883
963
  function explainHostsPassword() {
884
964
  console.log("Localghost may ask for your password to update its managed block in /etc/hosts.");
885
965
  console.log("It will only touch the lines between # localghost:start and # localghost:end.");
886
966
  }
967
+ function explainTrustPassword() {
968
+ console.log("Localghost can trust Caddy's local HTTPS CA so browsers stop showing local certificate warnings.");
969
+ console.log("macOS may ask for your password to add that local CA to Keychain.");
970
+ console.log("This only affects Caddy's local development certificates on this machine.");
971
+ }
887
972
  function useHttps(options) {
888
973
  return options.https === true || options.ssl === true;
889
974
  }
@@ -895,10 +980,10 @@ function getSetupCommand(options) {
895
980
  return `localghost setup${configFlags}${options.https ? " --https" : ""}`;
896
981
  }
897
982
  function getSetupReadiness(options) {
898
- const projectName = sanitizeProjectName(options.project ?? getProjectName(options.cwd));
983
+ const projectName = sanitizeProjectName(options.projectName ?? options.project ?? getProjectName(options.cwd));
899
984
  const readOptions = readOptionsFromCli(options);
900
- const entries = readDevHosts(readOptions);
901
- const configPath = resolveDevHostsPath(readOptions).path;
985
+ const entries = options.entries ?? readDevHosts(readOptions);
986
+ const configPath = options.configPath ?? resolveDevHostsPath(readOptions).path;
902
987
  const caddyfilePath = getCaddyfilePath(options.cwd);
903
988
  const statePath = getLocalghostStatePath(options.cwd);
904
989
  const state = readLocalghostState(options.cwd);
@@ -923,7 +1008,7 @@ function getSetupReadiness(options) {
923
1008
  reasons.push(`Could not read ${hostsPath}: ${message}`);
924
1009
  }
925
1010
  if (!options.ignoreCaddyfile) {
926
- if (!existsSync6(caddyfilePath)) {
1011
+ if (!existsSync7(caddyfilePath)) {
927
1012
  reasons.push(`Missing Caddyfile at ${caddyfilePath}.`);
928
1013
  } else {
929
1014
  const expectedCaddyfile = renderCaddyfile(entries, { https });
@@ -959,9 +1044,42 @@ async function runSetupFromReadiness(cwd, https, readiness) {
959
1044
  ...hostsResult.tempPath ? { hostsTempPath: hostsResult.tempPath } : {},
960
1045
  caddyfilePath,
961
1046
  caddyHttps: https,
1047
+ ...existingTrustMarkers(cwd),
962
1048
  entries: readiness.entries
963
1049
  });
964
1050
  }
1051
+ function wait(ms) {
1052
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1053
+ }
1054
+ async function runTrust(cwd, caddyfilePath) {
1055
+ await wait(350);
1056
+ try {
1057
+ await trustCaddy(caddyfilePath);
1058
+ } catch {
1059
+ await wait(750);
1060
+ await trustCaddy(caddyfilePath);
1061
+ }
1062
+ patchLocalghostState(cwd, { caddyTrustedAt: (/* @__PURE__ */ new Date()).toISOString() });
1063
+ console.log("Local HTTPS trust is ready.");
1064
+ }
1065
+ async function maybeTrustCaddy(options) {
1066
+ if (!options.https) return;
1067
+ const state = readLocalghostState(options.cwd);
1068
+ if (!options.trust && state?.caddyTrustedAt) return;
1069
+ let shouldTrust = options.trust === true;
1070
+ if (!shouldTrust) {
1071
+ if (state?.caddyTrustPromptedAt || !canPrompt()) return;
1072
+ explainTrustPassword();
1073
+ shouldTrust = await confirm("Trust local HTTPS certificates now?", true);
1074
+ }
1075
+ if (!shouldTrust) {
1076
+ patchLocalghostState(options.cwd, { caddyTrustPromptedAt: (/* @__PURE__ */ new Date()).toISOString() });
1077
+ console.log("Okay. Localghost will still run HTTPS, but the browser may show a certificate warning.");
1078
+ console.log("Run localghost trust when you want to trust Caddy's local CA.");
1079
+ return;
1080
+ }
1081
+ await runTrust(options.cwd, options.caddyfilePath);
1082
+ }
965
1083
  function maybePid(pid) {
966
1084
  return typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? pid : void 0;
967
1085
  }
@@ -1074,11 +1192,11 @@ program.command("update").description("Check npm for a newer localghost release"
1074
1192
  program.command("setup").description("Update /etc/hosts and generate/validate Caddyfile").option("--project <name>", "Managed /etc/hosts block name").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to look for. Can be repeated.", collect, []).option("--config-pattern <regex>", "Regex for config filenames in the project root").option("--https", "Generate a local HTTPS Caddy proxy with Caddy local certificates").option("--ssl", "Alias for --https").action(async (options) => {
1075
1193
  assertLocalDevelopment("setup");
1076
1194
  await assertCaddyReady();
1077
- const https = useHttps(options);
1078
- const projectName = sanitizeProjectName(options.project ?? getProjectName(options.cwd));
1079
- const readOptions = readOptionsFromCli(options);
1080
- const configPath = resolveDevHostsPath(readOptions).path;
1081
- const entries = readDevHosts(readOptions);
1195
+ const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
1196
+ const https = context.https;
1197
+ const projectName = context.projectName;
1198
+ const configPath = context.configPath;
1199
+ const entries = context.entries;
1082
1200
  warnAboutLocalMdns(entries);
1083
1201
  logDomainRoutes(entries, { https });
1084
1202
  explainHostsPassword();
@@ -1100,6 +1218,7 @@ program.command("setup").description("Update /etc/hosts and generate/validate Ca
1100
1218
  ...hostsResult.tempPath ? { hostsTempPath: hostsResult.tempPath } : {},
1101
1219
  caddyfilePath: caddyfile,
1102
1220
  caddyHttps: https,
1221
+ ...existingTrustMarkers(options.cwd),
1103
1222
  entries
1104
1223
  });
1105
1224
  console.log(`Generated ${caddyfile}`);
@@ -1107,6 +1226,20 @@ program.command("setup").description("Update /etc/hosts and generate/validate Ca
1107
1226
  console.log(`State ${statePath}`);
1108
1227
  console.log("Setup complete.");
1109
1228
  });
1229
+ program.command("trust").description("Trust Caddy's local HTTPS CA for this project's HTTPS proxy").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", "Use HTTPS mode for the Caddyfile").option("--ssl", "Alias for --https").action(async (options) => {
1230
+ assertLocalDevelopment("trust");
1231
+ await assertCaddyReady();
1232
+ const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
1233
+ if (!context.https) {
1234
+ throw new Error("Localghost HTTPS is not enabled for this context. Set https: true in localghost.config.mjs or pass --https.");
1235
+ }
1236
+ warnAboutLocalMdns(context.entries);
1237
+ logDomainRoutes(context.entries, { https: true });
1238
+ explainTrustPassword();
1239
+ const caddyfile = await writeCaddyfile(context.entries, options.cwd, { https: true });
1240
+ await validateCaddyfile(caddyfile);
1241
+ await runTrust(options.cwd, caddyfile);
1242
+ });
1110
1243
  program.command("reset").description("Remove Localghost setup state without deleting .localghost").option("--project <name>", "Managed /etc/hosts block name").option("--cwd <path>", "Project directory", process.cwd()).action(async (options) => {
1111
1244
  assertLocalDevelopment("reset");
1112
1245
  const projectName = sanitizeProjectName(options.project ?? getProjectName(options.cwd));
@@ -1114,13 +1247,13 @@ program.command("reset").description("Remove Localghost setup state without dele
1114
1247
  const statePath = getLocalghostStatePath(options.cwd);
1115
1248
  explainHostsPassword();
1116
1249
  const hostsResult = await removeSystemHosts(projectName);
1117
- if (existsSync6(caddyfilePath)) {
1250
+ if (existsSync7(caddyfilePath)) {
1118
1251
  unlinkSync(caddyfilePath);
1119
1252
  console.log(`Removed ${caddyfilePath}`);
1120
1253
  } else {
1121
1254
  console.log(`${caddyfilePath} was not present`);
1122
1255
  }
1123
- if (existsSync6(statePath)) {
1256
+ if (existsSync7(statePath)) {
1124
1257
  unlinkSync(statePath);
1125
1258
  console.log(`Removed ${statePath}`);
1126
1259
  } else {
@@ -1140,7 +1273,7 @@ program.command("teardown").description("Remove Localghost's managed /etc/hosts
1140
1273
  const hostsResult = await removeSystemHosts(projectName);
1141
1274
  const caddyfilePath = getCaddyfilePath(options.cwd);
1142
1275
  let caddyfileRemoved = false;
1143
- if (options.removeCaddyfile && existsSync6(caddyfilePath)) {
1276
+ if (options.removeCaddyfile && existsSync7(caddyfilePath)) {
1144
1277
  unlinkSync(caddyfilePath);
1145
1278
  caddyfileRemoved = true;
1146
1279
  }
@@ -1164,10 +1297,17 @@ program.command("teardown").description("Remove Localghost's managed /etc/hosts
1164
1297
  }
1165
1298
  console.log(`State ${statePath}`);
1166
1299
  });
1167
- 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((options) => {
1300
+ 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) => {
1168
1301
  const state = readLocalghostState(options.cwd);
1169
1302
  const statePath = getLocalghostStatePath(options.cwd);
1170
- const readiness = getSetupReadiness({ ...options, https: useHttps(options) });
1303
+ const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
1304
+ const readiness = getSetupReadiness({
1305
+ ...options,
1306
+ https: context.https,
1307
+ entries: context.entries,
1308
+ configPath: context.configPath,
1309
+ projectName: context.projectName
1310
+ });
1171
1311
  if (options.json) {
1172
1312
  console.log(JSON.stringify({ state, setup: readiness }, null, 2));
1173
1313
  return;
@@ -1183,6 +1323,8 @@ program.command("status").description("Print Localghost's project-local state fi
1183
1323
  if (state.hostsPath) console.log(`Hosts: ${state.hostsPath}`);
1184
1324
  if (state.caddyfilePath) console.log(`Caddyfile: ${state.caddyfilePath}`);
1185
1325
  if (typeof state.caddyHttps === "boolean") console.log(`Mode: ${state.caddyHttps ? "HTTPS" : "HTTP"}`);
1326
+ if (state.caddyTrustedAt) console.log(`HTTPS trust: yes (${state.caddyTrustedAt})`);
1327
+ if (!state.caddyTrustedAt && state.caddyTrustPromptedAt) console.log(`HTTPS trust: not enabled (asked ${state.caddyTrustPromptedAt})`);
1186
1328
  if (typeof state.caddyfileRemoved === "boolean") console.log(`Caddyfile removed: ${state.caddyfileRemoved}`);
1187
1329
  }
1188
1330
  if (readiness.ready) {
@@ -1198,16 +1340,23 @@ program.command("status").description("Print Localghost's project-local state fi
1198
1340
  process.exitCode = 1;
1199
1341
  }
1200
1342
  });
1201
- 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((options) => {
1202
- const entries = readDevHosts(readOptionsFromCli(options));
1203
- warnAboutLocalMdns(entries);
1204
- console.log(formatDomainRoutes(entries, { https: options.http ? false : useHttps(options) }));
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) => {
1344
+ const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
1345
+ warnAboutLocalMdns(context.entries);
1346
+ console.log(formatDomainRoutes(context.entries, { https: options.http ? false : context.https }));
1205
1347
  });
1206
- 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").action(async (options) => {
1348
+ 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) => {
1207
1349
  assertLocalDevelopment("dev");
1208
1350
  await assertCaddyReady();
1209
- const https = useHttps(options);
1210
- const readiness = getSetupReadiness({ ...options, https });
1351
+ const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
1352
+ const https = context.https;
1353
+ const readiness = getSetupReadiness({
1354
+ ...options,
1355
+ https,
1356
+ entries: context.entries,
1357
+ configPath: context.configPath,
1358
+ projectName: context.projectName
1359
+ });
1211
1360
  if (!readiness.ready) {
1212
1361
  if (!options.setup) {
1213
1362
  throw new Error(
@@ -1233,6 +1382,7 @@ program.command("dev").description("Run the Localghost Caddy proxy after setup")
1233
1382
  ...hostsResult.tempPath ? { hostsTempPath: hostsResult.tempPath } : {},
1234
1383
  caddyfilePath,
1235
1384
  caddyHttps: https,
1385
+ ...existingTrustMarkers(options.cwd),
1236
1386
  entries: readiness.entries
1237
1387
  });
1238
1388
  }
@@ -1241,6 +1391,17 @@ program.command("dev").description("Run the Localghost Caddy proxy after setup")
1241
1391
  const caddyfile = await writeCaddyfile(readiness.entries, options.cwd, { https });
1242
1392
  await validateCaddyfile(caddyfile);
1243
1393
  const caddy = startCaddy(caddyfile);
1394
+ try {
1395
+ await maybeTrustCaddy({
1396
+ cwd: options.cwd,
1397
+ https,
1398
+ caddyfilePath: caddyfile,
1399
+ ...typeof options.trust === "boolean" ? { trust: options.trust } : {}
1400
+ });
1401
+ } catch (error) {
1402
+ if (!caddy.killed) caddy.kill("SIGINT");
1403
+ throw error;
1404
+ }
1244
1405
  const caddyPid = maybePid(caddy.pid);
1245
1406
  const runRecord = registerLocalghostRun({
1246
1407
  mode: "dev",
@@ -1259,20 +1420,27 @@ program.command("dev").description("Run the Localghost Caddy proxy after setup")
1259
1420
  cleanupRun();
1260
1421
  }
1261
1422
  });
1262
- 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("--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) => {
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) => {
1263
1424
  assertLocalDevelopment("run");
1264
1425
  await assertCaddyReady();
1265
- const https = useHttps(options);
1266
1426
  const context = await resolveLocalghostContext({
1267
1427
  cwd: options.cwd,
1268
1428
  ...options.project ? { project: options.project } : {},
1269
1429
  ...options.config && options.config.length > 0 ? { configFiles: options.config } : {},
1270
1430
  ...options.configPattern ? { configPattern: options.configPattern } : {},
1271
1431
  ...options.port ? { port: options.port } : {},
1272
- https,
1432
+ ...useHttps(options) ? { https: true } : {},
1273
1433
  ...typeof options.dynamicPort === "boolean" ? { dynamicPort: options.dynamicPort } : {}
1274
1434
  });
1275
- const readiness = getSetupReadiness({ ...options, https, ignoreCaddyfile: true });
1435
+ const https = context.https;
1436
+ const readiness = getSetupReadiness({
1437
+ ...options,
1438
+ https,
1439
+ ignoreCaddyfile: true,
1440
+ entries: context.entries,
1441
+ configPath: context.configPath,
1442
+ projectName: context.projectName
1443
+ });
1276
1444
  if (!readiness.ready) {
1277
1445
  const shouldSetup = options.setup === true || canPrompt() && await confirm("Run caddy:setup now?", true);
1278
1446
  if (!shouldSetup) {
@@ -1298,6 +1466,17 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
1298
1466
  const caddyExit = caddy.catch((error) => {
1299
1467
  if (!caddy.killed) throw error;
1300
1468
  });
1469
+ try {
1470
+ await maybeTrustCaddy({
1471
+ cwd: options.cwd,
1472
+ https,
1473
+ caddyfilePath: caddyfile,
1474
+ ...typeof options.trust === "boolean" ? { trust: options.trust } : {}
1475
+ });
1476
+ } catch (error) {
1477
+ if (!caddy.killed) caddy.kill("SIGINT");
1478
+ throw error;
1479
+ }
1301
1480
  const [binary, ...args] = command;
1302
1481
  if (!binary) {
1303
1482
  throw new Error("Missing command. Use: localghost run -- vite");