@hamedb89/localghost 0.1.3 → 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,18 +1,112 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { existsSync as existsSync5, unlinkSync } from "fs";
4
+ import { existsSync as existsSync7, readFileSync as readFileSync6, unlinkSync } from "fs";
5
5
  import { Command, InvalidArgumentError } from "commander";
6
6
 
7
+ // src/activity.ts
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
9
+ import { homedir } from "os";
10
+ import { dirname, join } from "path";
11
+ var LOCALGHOST_ACTIVITY_VERSION = 1;
12
+ function getLocalghostActivityPath(env = process.env) {
13
+ if (env.LOCALGHOST_ACTIVITY_PATH) return env.LOCALGHOST_ACTIVITY_PATH;
14
+ const stateRoot = env.XDG_STATE_HOME || join(homedir(), ".local/state");
15
+ return join(stateRoot, "localghost", "activity.json");
16
+ }
17
+ function isProcessRunning(pid) {
18
+ if (!Number.isInteger(pid) || pid < 1) return false;
19
+ try {
20
+ process.kill(pid, 0);
21
+ return true;
22
+ } catch (error) {
23
+ const code = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
24
+ return code === "EPERM";
25
+ }
26
+ }
27
+ function emptyActivity() {
28
+ return { version: LOCALGHOST_ACTIVITY_VERSION, runs: [] };
29
+ }
30
+ function readLocalghostActivity(path = getLocalghostActivityPath()) {
31
+ if (!existsSync(path)) return emptyActivity();
32
+ try {
33
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
34
+ return {
35
+ version: LOCALGHOST_ACTIVITY_VERSION,
36
+ runs: Array.isArray(parsed.runs) ? parsed.runs : []
37
+ };
38
+ } catch {
39
+ return emptyActivity();
40
+ }
41
+ }
42
+ function writeLocalghostActivity(activity, path = getLocalghostActivityPath()) {
43
+ mkdirSync(dirname(path), { recursive: true });
44
+ writeFileSync(path, `${JSON.stringify(activity, null, 2)}
45
+ `, "utf8");
46
+ return path;
47
+ }
48
+ function createRunId(input2, pid) {
49
+ return `${input2.projectName}:${input2.mode}:${pid}:${Date.now()}`;
50
+ }
51
+ function pruneLocalghostActivity(path = getLocalghostActivityPath()) {
52
+ const activity = readLocalghostActivity(path);
53
+ const activeRuns = activity.runs.filter((run) => isProcessRunning(run.pid));
54
+ const pruned = activeRuns.length !== activity.runs.length;
55
+ if (pruned) {
56
+ writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs: activeRuns }, path);
57
+ }
58
+ return {
59
+ path,
60
+ pruned,
61
+ runs: activeRuns
62
+ };
63
+ }
64
+ function listLocalghostRuns(path = getLocalghostActivityPath()) {
65
+ return pruneLocalghostActivity(path).runs;
66
+ }
67
+ function registerLocalghostRun(input2, path = getLocalghostActivityPath()) {
68
+ const now = (/* @__PURE__ */ new Date()).toISOString();
69
+ const pid = input2.pid ?? process.pid;
70
+ const record = {
71
+ id: input2.id ?? createRunId(input2, pid),
72
+ mode: input2.mode,
73
+ pid,
74
+ cwd: input2.cwd,
75
+ projectName: input2.projectName,
76
+ startedAt: input2.startedAt ?? now,
77
+ updatedAt: now,
78
+ ...input2.configPath ? { configPath: input2.configPath } : {},
79
+ ...input2.caddyfilePath ? { caddyfilePath: input2.caddyfilePath } : {},
80
+ ...input2.caddyPid ? { caddyPid: input2.caddyPid } : {},
81
+ ...input2.childPid ? { childPid: input2.childPid } : {},
82
+ ...input2.childCommand ? { childCommand: input2.childCommand } : {},
83
+ ...typeof input2.https === "boolean" ? { https: input2.https } : {},
84
+ ...input2.requestedPort ? { requestedPort: input2.requestedPort } : {},
85
+ ...input2.port ? { port: input2.port } : {},
86
+ ...typeof input2.dynamicPort === "boolean" ? { dynamicPort: input2.dynamicPort } : {},
87
+ entries: input2.entries
88
+ };
89
+ const current = pruneLocalghostActivity(path).runs.filter((run) => run.id !== record.id);
90
+ writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs: [...current, record] }, path);
91
+ return record;
92
+ }
93
+ function unregisterLocalghostRun(id, path = getLocalghostActivityPath()) {
94
+ const activity = readLocalghostActivity(path);
95
+ const runs = activity.runs.filter((run) => run.id !== id);
96
+ if (runs.length !== activity.runs.length) {
97
+ writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs }, path);
98
+ }
99
+ }
100
+
7
101
  // src/config.ts
8
- import { existsSync, readFileSync, readdirSync } from "fs";
9
- import { basename, join, resolve } from "path";
102
+ import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
103
+ import { basename, join as join2, resolve } from "path";
10
104
 
11
105
  // src/parse.ts
12
106
  var HOST_PATTERN = /^(?=.{1,253}$)(?!-)[a-z0-9-]+(\.[a-z0-9-]+)*\.?$/i;
13
- function parseDevHosts(input, fileName = ".localghost") {
107
+ function parseDevHosts(input2, fileName = ".localghost") {
14
108
  const entries = [];
15
- input.split(/\r?\n/).forEach((rawLine, index) => {
109
+ input2.split(/\r?\n/).forEach((rawLine, index) => {
16
110
  const line = rawLine.replace(/#.*/, "").trim();
17
111
  if (!line) {
18
112
  return;
@@ -74,7 +168,7 @@ function resolveDevHostsPath(options = {}) {
74
168
  const searchedFiles = getConfigFileCandidates(options);
75
169
  for (const fileName2 of searchedFiles) {
76
170
  const path = resolve(cwd, fileName2);
77
- if (existsSync(path)) {
171
+ if (existsSync2(path)) {
78
172
  return {
79
173
  path,
80
174
  fileName: basename(fileName2),
@@ -107,11 +201,11 @@ function readDevHosts(options = {}) {
107
201
  `Missing Localghost config in ${cwd}. Looked for ${formatSearchedFiles(resolvedPath.searchedFiles, resolvedPath.configPattern)}. Run \`localghost init\` or pass --config/--config-pattern.`
108
202
  );
109
203
  }
110
- return parseDevHosts(readFileSync(resolvedPath.path, "utf8"), resolvedPath.fileName);
204
+ return parseDevHosts(readFileSync2(resolvedPath.path, "utf8"), resolvedPath.fileName);
111
205
  }
112
206
  function getProjectName(cwd = process.cwd()) {
113
207
  try {
114
- const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
208
+ const pkg = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
115
209
  const name = typeof pkg.name === "string" && pkg.name ? pkg.name : "app";
116
210
  return sanitizeProjectName(name.replace(/^@/, ""));
117
211
  } catch {
@@ -124,18 +218,18 @@ function sanitizeProjectName(value) {
124
218
  }
125
219
 
126
220
  // src/caddy.ts
127
- import { dirname as dirname2, join as join2 } from "path";
221
+ import { dirname as dirname3, join as join3 } from "path";
128
222
  import { execa } from "execa";
129
223
 
130
224
  // src/fs.ts
131
- import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
132
- import { dirname } from "path";
225
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
226
+ import { dirname as dirname2 } from "path";
133
227
  function readTextFile(path) {
134
- return readFileSync2(path, "utf8");
228
+ return readFileSync3(path, "utf8");
135
229
  }
136
230
  function writeTextFile(path, value) {
137
- mkdirSync(dirname(path), { recursive: true });
138
- writeFileSync(path, value, "utf8");
231
+ mkdirSync2(dirname2(path), { recursive: true });
232
+ writeFileSync2(path, value, "utf8");
139
233
  return path;
140
234
  }
141
235
 
@@ -150,41 +244,189 @@ function groupByPort(entries) {
150
244
  return groups;
151
245
  }
152
246
  function getCaddyfilePath(cwd = process.cwd()) {
153
- return join2(cwd, "ops/local/Caddyfile");
247
+ return join3(cwd, "ops/local/Caddyfile");
154
248
  }
155
- function renderCaddyfile(entries) {
249
+ function renderCaddyfile(entries, options = {}) {
156
250
  const groups = groupByPort(entries);
251
+ const https = options.https === true;
157
252
  const blocks = [...groups.entries()].sort(([leftPort], [rightPort]) => leftPort - rightPort).map(([port, group]) => {
158
- const hosts = group.map((entry) => entry.host).sort().join(", ");
253
+ const hosts = group.map((entry) => https ? entry.host : `http://${entry.host}`).sort().join(", ");
159
254
  return `${hosts} {
160
255
  reverse_proxy 127.0.0.1:${port}
161
256
  }`;
162
257
  });
163
- return `{
258
+ const globalOptions = https ? `{
164
259
  local_certs
165
260
  }
166
261
 
167
- ${blocks.join("\n\n")}
262
+ ` : "";
263
+ return `${globalOptions}${blocks.join("\n\n")}
168
264
  `;
169
265
  }
170
- async function writeCaddyfile(entries, cwd = process.cwd()) {
266
+ async function writeCaddyfile(entries, cwd = process.cwd(), options = {}) {
171
267
  const path = getCaddyfilePath(cwd);
172
- writeTextFile(path, renderCaddyfile(entries));
268
+ writeTextFile(path, renderCaddyfile(entries, options));
173
269
  return path;
174
270
  }
175
271
  async function validateCaddyfile(path) {
176
272
  await execa("caddy", ["validate", "--config", path], {
177
- cwd: dirname2(path),
273
+ cwd: dirname3(path),
274
+ stdio: "inherit"
275
+ });
276
+ }
277
+ function startCaddy(path) {
278
+ return execa("caddy", ["run", "--config", path], {
279
+ cwd: dirname3(path),
178
280
  stdio: "inherit"
179
281
  });
180
282
  }
181
- async function runCaddy(path) {
182
- await execa("caddy", ["run", "--config", path], {
183
- cwd: dirname2(path),
283
+ async function trustCaddy(path) {
284
+ await execa("caddy", ["trust", "--config", path], {
285
+ cwd: dirname3(path),
184
286
  stdio: "inherit"
185
287
  });
186
288
  }
187
289
 
290
+ // src/context.ts
291
+ import { existsSync as existsSync3 } from "fs";
292
+ import { pathToFileURL } from "url";
293
+
294
+ // src/port.ts
295
+ import { createServer } from "net";
296
+ async function isPortAvailable(port, host = "127.0.0.1") {
297
+ return new Promise((resolve2) => {
298
+ const server = createServer();
299
+ server.once("error", () => {
300
+ resolve2(false);
301
+ });
302
+ server.once("listening", () => {
303
+ server.close(() => resolve2(true));
304
+ });
305
+ server.listen(port, host);
306
+ });
307
+ }
308
+ async function findAvailablePort(startPort, options = {}) {
309
+ const host = options.host ?? "127.0.0.1";
310
+ const maxAttempts = options.maxAttempts ?? 50;
311
+ for (let offset = 0; offset < maxAttempts; offset += 1) {
312
+ const port = startPort + offset;
313
+ if (await isPortAvailable(port, host)) {
314
+ return port;
315
+ }
316
+ }
317
+ throw new Error(`No available port found from ${startPort} to ${startPort + maxAttempts - 1}.`);
318
+ }
319
+
320
+ // src/context.ts
321
+ var LOCALGHOST_PROJECT_CONFIG_FILES = [
322
+ "localghost.config.mjs",
323
+ "localghost.config.js",
324
+ "localghost.config.cjs"
325
+ ];
326
+ function parsePort(value) {
327
+ if (!value) return void 0;
328
+ const port = Number.parseInt(value, 10);
329
+ return Number.isInteger(port) && port > 0 && port <= 65535 ? port : void 0;
330
+ }
331
+ function envPort() {
332
+ return parsePort(process.env.LOCALGHOST_PORT) ?? parsePort(process.env.VITE_PORT);
333
+ }
334
+ function envDynamicPort() {
335
+ const value = process.env.LOCALGHOST_DYNAMIC_PORT;
336
+ if (!value) return void 0;
337
+ return ["1", "true", "yes", "on"].includes(value.toLowerCase());
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
+ }
344
+ function readOptionsFromContext(options) {
345
+ return {
346
+ cwd: options.cwd ?? process.cwd(),
347
+ ...options.fileName ? { fileName: options.fileName } : {},
348
+ ...options.configFiles ? { configFiles: options.configFiles } : {},
349
+ ...options.configPattern ? { configPattern: options.configPattern } : {}
350
+ };
351
+ }
352
+ function withRuntimePort(entries, requestedPort, port) {
353
+ if (requestedPort === port) return entries;
354
+ const hasRequestedPort = entries.some((entry) => entry.port === requestedPort);
355
+ if (!hasRequestedPort) return entries;
356
+ return entries.map((entry) => entry.port === requestedPort ? { ...entry, port } : entry);
357
+ }
358
+ function uniqueHosts(entries) {
359
+ return [...new Set(entries.map((entry) => entry.host))];
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
+ }
391
+ async function resolveLocalghostContext(options = {}) {
392
+ const cwd = options.cwd ?? process.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 });
399
+ const resolvedPath = resolveDevHostsPath(readOptions);
400
+ const configEntries = readDevHosts(readOptions);
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";
404
+ const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
405
+ const port = dynamicPort ? await findAvailablePort(requestedPort, { host: probeHost }) : requestedPort;
406
+ const wwwAlias = merged.wwwAlias ?? true;
407
+ const entries = wwwAlias ? addDefaultWwwAliases(withRuntimePort(configEntries, requestedPort, port)) : withRuntimePort(configEntries, requestedPort, port);
408
+ const hosts = uniqueHosts(entries);
409
+ const primaryHost = merged.primaryHost ?? entries.find((entry) => entry.port === port)?.host ?? hosts[0] ?? `${sanitizeProjectName(getProjectName(cwd))}.localhost`;
410
+ return {
411
+ cwd,
412
+ projectName: sanitizeProjectName(merged.project ?? getProjectName(cwd)),
413
+ readOptions,
414
+ configPath: resolvedPath.path,
415
+ configFileName: resolvedPath.fileName,
416
+ configEntries,
417
+ entries,
418
+ hosts,
419
+ requestedPort,
420
+ port,
421
+ dynamicPort,
422
+ bindHost,
423
+ primaryHost,
424
+ https: merged.https ?? envHttps() ?? false,
425
+ wwwAlias,
426
+ ...projectConfig.path ? { projectConfigPath: projectConfig.path } : {}
427
+ };
428
+ }
429
+
188
430
  // src/doctor.ts
189
431
  import { execa as execa2 } from "execa";
190
432
  async function checkCaddy() {
@@ -211,10 +453,27 @@ async function runDoctor() {
211
453
  };
212
454
  }
213
455
 
456
+ // src/env.ts
457
+ function getProductionReason(env = process.env) {
458
+ if (env.LOCALGHOST_ENV === "production") return "LOCALGHOST_ENV=production";
459
+ if (env.NODE_ENV === "production") return "NODE_ENV=production";
460
+ if (env.VERCEL_ENV === "production") return "VERCEL_ENV=production";
461
+ if (env.NETLIFY === "true" && env.CONTEXT === "production") return "NETLIFY=true and CONTEXT=production";
462
+ if (env.CF_PAGES_BRANCH && env.CF_PAGES_BRANCH === env.CF_PAGES_PRODUCTION_BRANCH) {
463
+ return "CF_PAGES_BRANCH matches CF_PAGES_PRODUCTION_BRANCH";
464
+ }
465
+ return null;
466
+ }
467
+ function assertLocalDevelopment(command, env = process.env) {
468
+ const reason = getProductionReason(env);
469
+ if (!reason) return;
470
+ throw new Error(`Localghost only runs in local development. Refusing \`${command}\` because ${reason}.`);
471
+ }
472
+
214
473
  // src/hosts-file.ts
215
- import { writeFileSync as writeFileSync2 } from "fs";
474
+ import { writeFileSync as writeFileSync3 } from "fs";
216
475
  import { tmpdir } from "os";
217
- import { join as join3 } from "path";
476
+ import { join as join4 } from "path";
218
477
  import { execa as execa3 } from "execa";
219
478
  function escapeRegExp(value) {
220
479
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -256,8 +515,8 @@ function removeManagedBlock(existing, projectName) {
256
515
  }
257
516
  async function writeSystemHostsFile(hostsPath, next, projectName) {
258
517
  const sanitizedProjectName = sanitizeProjectName(projectName);
259
- const tempPath = join3(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
260
- writeFileSync2(tempPath, next, "utf8");
518
+ const tempPath = join4(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
519
+ writeFileSync3(tempPath, next, "utf8");
261
520
  if (process.platform === "win32") {
262
521
  throw new Error(`Windows support: run as administrator and copy ${tempPath} to ${hostsPath}.`);
263
522
  }
@@ -289,11 +548,11 @@ async function removeSystemHosts(projectName) {
289
548
  }
290
549
 
291
550
  // src/init.ts
292
- import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
293
- import { join as join4 } from "path";
551
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
552
+ import { join as join5 } from "path";
294
553
  function detectPackageManager(cwd = process.cwd()) {
295
- if (existsSync2(join4(cwd, "pnpm-lock.yaml"))) return "pnpm";
296
- if (existsSync2(join4(cwd, "yarn.lock"))) return "yarn";
554
+ if (existsSync4(join5(cwd, "pnpm-lock.yaml"))) return "pnpm";
555
+ if (existsSync4(join5(cwd, "yarn.lock"))) return "yarn";
297
556
  return "npm";
298
557
  }
299
558
  function packageRunCommand(packageManager, script) {
@@ -313,7 +572,7 @@ function renderConfig(options) {
313
572
  }
314
573
  function readPackageJson(path) {
315
574
  try {
316
- return JSON.parse(readFileSync3(path, "utf8"));
575
+ return JSON.parse(readFileSync4(path, "utf8"));
317
576
  } catch {
318
577
  return null;
319
578
  }
@@ -334,17 +593,25 @@ function updatePackageScripts(packageJsonPath, configFile) {
334
593
  ...scripts,
335
594
  "localghost:setup": scripts["localghost:setup"] ?? `localghost setup${configFlag}`,
336
595
  "localghost:proxy": scripts["localghost:proxy"] ?? `localghost dev${configFlag}`,
596
+ "localghost:proxy:https": scripts["localghost:proxy:https"] ?? `localghost dev${configFlag} --https`,
597
+ "localghost:run": scripts["localghost:run"] ?? `localghost run${configFlag} --`,
598
+ "localghost:ready": scripts["localghost:ready"] ?? `localghost status${configFlag} --ready`,
599
+ "localghost:trust": scripts["localghost:trust"] ?? `localghost trust${configFlag}`,
600
+ "localghost:ps": scripts["localghost:ps"] ?? "localghost ps",
337
601
  "localghost:print": scripts["localghost:print"] ?? `localghost print${configFlag}`,
338
602
  "localghost:routes": scripts["localghost:routes"] ?? `localghost routes${configFlag}`,
339
603
  "localghost:status": scripts["localghost:status"] ?? "localghost status",
604
+ "localghost:reset": scripts["localghost:reset"] ?? "localghost reset",
340
605
  "localghost:teardown": scripts["localghost:teardown"] ?? "localghost teardown",
341
606
  "localghost:doctor": scripts["localghost:doctor"] ?? "localghost doctor",
342
- "localghost:update": scripts["localghost:update"] ?? "localghost update"
607
+ "localghost:update": scripts["localghost:update"] ?? "localghost update",
608
+ "caddy:setup": scripts["caddy:setup"] ?? `localghost setup${configFlag}`,
609
+ "caddy:dev": scripts["caddy:dev"] ?? `localghost dev${configFlag}`
343
610
  };
344
611
  const changed = JSON.stringify(scripts) !== JSON.stringify(nextScripts);
345
612
  if (!changed) return false;
346
613
  pkg.scripts = nextScripts;
347
- writeFileSync3(packageJsonPath, `${JSON.stringify(pkg, null, 2)}
614
+ writeFileSync4(packageJsonPath, `${JSON.stringify(pkg, null, 2)}
348
615
  `, "utf8");
349
616
  return true;
350
617
  }
@@ -357,8 +624,8 @@ function initLocalghost(options = {}) {
357
624
  const apiPort = options.apiPort ?? 8787;
358
625
  const packageManager = options.packageManager ?? detectPackageManager(cwd);
359
626
  const configFile = options.configFile ?? LOCALGHOST_CONFIG_FILE;
360
- const configPath = join4(cwd, configFile);
361
- const configExists = existsSync2(configPath);
627
+ const configPath = join5(cwd, configFile);
628
+ const configExists = existsSync4(configPath);
362
629
  if (configExists && !options.force) {
363
630
  return {
364
631
  configPath,
@@ -368,30 +635,55 @@ function initLocalghost(options = {}) {
368
635
  nextSteps: [
369
636
  packageRunCommand(packageManager, "localghost:doctor"),
370
637
  packageRunCommand(packageManager, "localghost:setup"),
638
+ packageRunCommand(packageManager, "localghost:ready"),
371
639
  packageRunCommand(packageManager, "localghost:proxy")
372
640
  ]
373
641
  };
374
642
  }
375
643
  writeTextFile(configPath, renderConfig({ host, port, apiHost, apiPort }));
376
- const packageJsonPath = join4(cwd, "package.json");
644
+ const packageJsonPath = join5(cwd, "package.json");
377
645
  const packageJsonChanged = options.writeScripts ? updatePackageScripts(packageJsonPath, configFile) : false;
378
646
  return {
379
647
  configPath,
380
648
  configCreated: true,
381
- ...existsSync2(packageJsonPath) ? { packageJsonPath } : {},
649
+ ...existsSync4(packageJsonPath) ? { packageJsonPath } : {},
382
650
  packageJsonChanged,
383
651
  packageManager,
384
652
  nextSteps: [
385
653
  packageRunCommand(packageManager, "localghost:doctor"),
386
654
  packageRunCommand(packageManager, "localghost:setup"),
655
+ packageRunCommand(packageManager, "localghost:ready"),
387
656
  packageRunCommand(packageManager, "localghost:proxy")
388
657
  ]
389
658
  };
390
659
  }
391
660
 
661
+ // src/prompt.ts
662
+ import { stdin as input, stdout as output } from "process";
663
+ import { createInterface } from "readline/promises";
664
+ function canPrompt() {
665
+ return Boolean(input.isTTY && output.isTTY);
666
+ }
667
+ async function withPrompt(run) {
668
+ const rl = createInterface({ input, output });
669
+ try {
670
+ return await run((question) => rl.question(question));
671
+ } finally {
672
+ rl.close();
673
+ }
674
+ }
675
+ async function confirm(question, defaultValue = true) {
676
+ return withPrompt(async (prompt) => {
677
+ const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
678
+ const answer = (await prompt(`${question}${suffix}`)).trim().toLowerCase();
679
+ if (!answer) return defaultValue;
680
+ return answer === "y" || answer === "yes";
681
+ });
682
+ }
683
+
392
684
  // src/routes.ts
393
685
  function getDomainRoutes(entries, options = {}) {
394
- const protocol = options.https === false ? "http" : "https";
686
+ const protocol = options.https === true ? "https" : "http";
395
687
  return [...entries].sort((left, right) => left.host.localeCompare(right.host) || left.port - right.port).map((entry) => ({
396
688
  host: entry.host,
397
689
  port: entry.port,
@@ -411,30 +703,35 @@ function formatDomainRoutes(entries, options = {}) {
411
703
  }
412
704
 
413
705
  // src/state.ts
414
- import { existsSync as existsSync3 } from "fs";
415
- import { join as join5 } from "path";
706
+ import { existsSync as existsSync5 } from "fs";
707
+ import { join as join6 } from "path";
416
708
  var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
417
709
  function getLocalghostStatePath(cwd = process.cwd()) {
418
- return join5(cwd, LOCALGHOST_STATE_FILE);
710
+ return join6(cwd, LOCALGHOST_STATE_FILE);
419
711
  }
420
712
  function readLocalghostState(cwd = process.cwd()) {
421
713
  const path = getLocalghostStatePath(cwd);
422
- if (!existsSync3(path)) return null;
714
+ if (!existsSync5(path)) return null;
423
715
  return JSON.parse(readTextFile(path));
424
716
  }
425
717
  function writeLocalghostState(cwd, state) {
426
718
  const path = getLocalghostStatePath(cwd);
427
- 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)}
428
720
  `);
429
721
  return path;
430
722
  }
723
+ function patchLocalghostState(cwd, patch) {
724
+ const current = readLocalghostState(cwd);
725
+ if (!current) return null;
726
+ return writeLocalghostState(cwd, { ...current, ...patch });
727
+ }
431
728
 
432
729
  // src/update-check.ts
433
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
434
- import { homedir } from "os";
435
- import { dirname as dirname3, join as join6 } from "path";
730
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
731
+ import { homedir as homedir2 } from "os";
732
+ import { dirname as dirname4, join as join7 } from "path";
436
733
  var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
437
- var LOCALGHOST_VERSION = "0.1.0";
734
+ var LOCALGHOST_VERSION = "0.1.8";
438
735
  var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
439
736
  var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
440
737
  var UPDATE_CHECK_TIMEOUT_MS = 900;
@@ -446,21 +743,21 @@ function isUpdateCheckDisabled(env = process.env) {
446
743
  }
447
744
  function getUpdateCheckCachePath(env = process.env) {
448
745
  if (env.LOCALGHOST_UPDATE_CHECK_CACHE) return env.LOCALGHOST_UPDATE_CHECK_CACHE;
449
- const cacheRoot = env.XDG_CACHE_HOME || join6(homedir(), ".cache");
450
- return join6(cacheRoot, "localghost", "update-check.json");
746
+ const cacheRoot = env.XDG_CACHE_HOME || join7(homedir2(), ".cache");
747
+ return join7(cacheRoot, "localghost", "update-check.json");
451
748
  }
452
749
  function readCache(path = getUpdateCheckCachePath()) {
453
- if (!existsSync4(path)) return null;
750
+ if (!existsSync6(path)) return null;
454
751
  try {
455
- return JSON.parse(readFileSync4(path, "utf8"));
752
+ return JSON.parse(readFileSync5(path, "utf8"));
456
753
  } catch {
457
754
  return null;
458
755
  }
459
756
  }
460
757
  function writeCache(cache, path = getUpdateCheckCachePath()) {
461
758
  try {
462
- mkdirSync2(dirname3(path), { recursive: true });
463
- writeFileSync4(path, `${JSON.stringify(cache, null, 2)}
759
+ mkdirSync3(dirname4(path), { recursive: true });
760
+ writeFileSync5(path, `${JSON.stringify(cache, null, 2)}
464
761
  `, "utf8");
465
762
  } catch {
466
763
  }
@@ -597,6 +894,7 @@ ${message}`);
597
894
  }
598
895
 
599
896
  // src/cli.ts
897
+ import { execa as execa4 } from "execa";
600
898
  function warnAboutLocalMdns(entries) {
601
899
  const localHosts = findLocalMdnsHosts(entries);
602
900
  if (localHosts.length > 0) {
@@ -605,10 +903,10 @@ function warnAboutLocalMdns(entries) {
605
903
  );
606
904
  }
607
905
  }
608
- function logDomainRoutes(entries) {
609
- console.log(formatDomainRoutes(entries));
906
+ function logDomainRoutes(entries, options = {}) {
907
+ console.log(formatDomainRoutes(entries, options));
610
908
  }
611
- function parsePort(value) {
909
+ function parsePort2(value) {
612
910
  const port = Number.parseInt(value, 10);
613
911
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
614
912
  throw new InvalidArgumentError("Port must be a number between 1 and 65535.");
@@ -622,6 +920,23 @@ function parsePackageManager(value) {
622
920
  function collect(value, previous = []) {
623
921
  return [...previous, value];
624
922
  }
923
+ function parseBooleanLike(value) {
924
+ if (value === true) return true;
925
+ if (value === false) return false;
926
+ const normalized = value.toLowerCase();
927
+ if (["1", "true", "yes", "y", "on"].includes(normalized)) return true;
928
+ if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
929
+ throw new InvalidArgumentError("Value must be yes or no.");
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
+ }
625
940
  function readOptionsFromCli(options) {
626
941
  return {
627
942
  cwd: options.cwd,
@@ -638,6 +953,183 @@ async function assertCaddyReady() {
638
953
  "Localghost will not install it for you. No surprise spells."
639
954
  ].join("\n"));
640
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
+ }
963
+ function explainHostsPassword() {
964
+ console.log("Localghost may ask for your password to update its managed block in /etc/hosts.");
965
+ console.log("It will only touch the lines between # localghost:start and # localghost:end.");
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
+ }
972
+ function useHttps(options) {
973
+ return options.https === true || options.ssl === true;
974
+ }
975
+ function getSetupCommand(options) {
976
+ const configFlags = [
977
+ ...(options.config ?? []).map((config) => ` --config ${config}`),
978
+ ...options.configPattern ? [` --config-pattern ${options.configPattern}`] : []
979
+ ].join("");
980
+ return `localghost setup${configFlags}${options.https ? " --https" : ""}`;
981
+ }
982
+ function getSetupReadiness(options) {
983
+ const projectName = sanitizeProjectName(options.projectName ?? options.project ?? getProjectName(options.cwd));
984
+ const readOptions = readOptionsFromCli(options);
985
+ const entries = options.entries ?? readDevHosts(readOptions);
986
+ const configPath = options.configPath ?? resolveDevHostsPath(readOptions).path;
987
+ const caddyfilePath = getCaddyfilePath(options.cwd);
988
+ const statePath = getLocalghostStatePath(options.cwd);
989
+ const state = readLocalghostState(options.cwd);
990
+ const https = options.https === true;
991
+ const reasons = [];
992
+ if (!state) {
993
+ reasons.push(`No Localghost setup state found at ${statePath}.`);
994
+ } else {
995
+ if (state.action !== "setup") reasons.push(`Last Localghost action is ${state.action}, not setup.`);
996
+ if (state.projectName !== projectName) reasons.push(`Setup state is for project ${state.projectName}, not ${projectName}.`);
997
+ if (state.configPath !== configPath) reasons.push(`Setup state points at ${state.configPath ?? "no config"}, not ${configPath}.`);
998
+ }
999
+ const hostsPath = getSystemHostsPath();
1000
+ try {
1001
+ const hosts = readFileSync6(hostsPath, "utf8");
1002
+ const expectedHostsBlock = renderHostsBlock(projectName, entries).trimEnd();
1003
+ if (!hosts.includes(expectedHostsBlock)) {
1004
+ reasons.push(`The Localghost hosts block in ${hostsPath} is missing or stale.`);
1005
+ }
1006
+ } catch (error) {
1007
+ const message = error instanceof Error ? error.message : String(error);
1008
+ reasons.push(`Could not read ${hostsPath}: ${message}`);
1009
+ }
1010
+ if (!options.ignoreCaddyfile) {
1011
+ if (!existsSync7(caddyfilePath)) {
1012
+ reasons.push(`Missing Caddyfile at ${caddyfilePath}.`);
1013
+ } else {
1014
+ const expectedCaddyfile = renderCaddyfile(entries, { https });
1015
+ const currentCaddyfile = readFileSync6(caddyfilePath, "utf8");
1016
+ if (currentCaddyfile !== expectedCaddyfile) {
1017
+ reasons.push(`Caddyfile at ${caddyfilePath} is stale for ${https ? "HTTPS" : "HTTP"} mode.`);
1018
+ }
1019
+ }
1020
+ }
1021
+ return {
1022
+ ready: reasons.length === 0,
1023
+ reasons,
1024
+ entries,
1025
+ projectName,
1026
+ configPath,
1027
+ caddyfilePath,
1028
+ statePath,
1029
+ setupCommand: getSetupCommand(options)
1030
+ };
1031
+ }
1032
+ async function runSetupFromReadiness(cwd, https, readiness) {
1033
+ explainHostsPassword();
1034
+ const hostsResult = await updateSystemHosts(readiness.projectName, readiness.entries);
1035
+ const caddyfilePath = await writeCaddyfile(readiness.entries, cwd, { https });
1036
+ await validateCaddyfile(caddyfilePath);
1037
+ writeLocalghostState(cwd, {
1038
+ action: "setup",
1039
+ projectName: readiness.projectName,
1040
+ cwd,
1041
+ configPath: readiness.configPath,
1042
+ hostsPath: hostsResult.hostsPath,
1043
+ hostsChanged: hostsResult.changed,
1044
+ ...hostsResult.tempPath ? { hostsTempPath: hostsResult.tempPath } : {},
1045
+ caddyfilePath,
1046
+ caddyHttps: https,
1047
+ ...existingTrustMarkers(cwd),
1048
+ entries: readiness.entries
1049
+ });
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
+ }
1083
+ function maybePid(pid) {
1084
+ return typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? pid : void 0;
1085
+ }
1086
+ function registerCleanup(id) {
1087
+ let cleaned = false;
1088
+ const cleanup = () => {
1089
+ if (cleaned) return;
1090
+ cleaned = true;
1091
+ unregisterLocalghostRun(id);
1092
+ };
1093
+ process.once("exit", cleanup);
1094
+ return () => {
1095
+ cleanup();
1096
+ process.off("exit", cleanup);
1097
+ };
1098
+ }
1099
+ async function getRunView(run) {
1100
+ const portStatus = /* @__PURE__ */ new Map();
1101
+ for (const entry of run.entries) {
1102
+ if (!portStatus.has(entry.port)) {
1103
+ portStatus.set(entry.port, !await isPortAvailable(entry.port));
1104
+ }
1105
+ }
1106
+ 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
+ }))
1114
+ };
1115
+ }
1116
+ function formatRunViews(runs) {
1117
+ if (runs.length === 0) return "No Localghost apps are running.";
1118
+ 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;
1122
+ 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) {
1128
+ lines.push(` ${route.host} -> ${route.target} (${route.listening ? "listening" : "not listening"})`);
1129
+ }
1130
+ }
1131
+ return lines.join("\n");
1132
+ }
641
1133
  var program = new Command();
642
1134
  program.name("localghost").description("Buh. Friendly local hostnames for app repos.").version(LOCALGHOST_VERSION).option("--no-update-check", "Skip the npm update check for this run");
643
1135
  program.hook("postAction", async (_thisCommand, actionCommand) => {
@@ -645,7 +1137,7 @@ program.hook("postAction", async (_thisCommand, actionCommand) => {
645
1137
  const options = program.opts();
646
1138
  await maybeNotifyAboutUpdate({ disabled: options.updateCheck === false });
647
1139
  });
648
- program.command("init").description("Create a .localghost config for this project").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to create", ".localghost").option("--host <host>", "Primary local hostname").option("--port <number>", "Primary app port", parsePort).option("--api-host <host>", "API local hostname").option("--api-port <number>", "API port", parsePort).option("--package-manager <npm|yarn|pnpm>", "Package manager for suggested commands", parsePackageManager).option("--write-scripts", "Add localghost scripts to package.json").option("--force", "Overwrite an existing config file").action((options) => {
1140
+ program.command("init").description("Create a .localghost config for this project").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to create", ".localghost").option("--host <host>", "Primary local hostname").option("--port <number>", "Primary app port", parsePort2).option("--api-host <host>", "API local hostname").option("--api-port <number>", "API port", parsePort2).option("--package-manager <npm|yarn|pnpm>", "Package manager for suggested commands", parsePackageManager).option("--write-scripts", "Add localghost scripts to package.json").option("--force", "Overwrite an existing config file").action((options) => {
649
1141
  const result = initLocalghost({ ...options, configFile: options.config });
650
1142
  if (result.configCreated) {
651
1143
  console.log(`Buh. Created ${result.configPath}`);
@@ -697,21 +1189,24 @@ program.command("update").description("Check npm for a newer localghost release"
697
1189
  }
698
1190
  console.log(`localghost is up to date. Current: ${result.currentVersion}`);
699
1191
  });
700
- 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").action(async (options) => {
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) => {
1193
+ assertLocalDevelopment("setup");
701
1194
  await assertCaddyReady();
702
- const projectName = sanitizeProjectName(options.project ?? getProjectName(options.cwd));
703
- const readOptions = readOptionsFromCli(options);
704
- const configPath = resolveDevHostsPath(readOptions).path;
705
- 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;
706
1200
  warnAboutLocalMdns(entries);
707
- logDomainRoutes(entries);
1201
+ logDomainRoutes(entries, { https });
1202
+ explainHostsPassword();
708
1203
  const hostsResult = await updateSystemHosts(projectName, entries);
709
1204
  if (hostsResult.changed) {
710
1205
  console.log(`Updated ${hostsResult.hostsPath}`);
711
1206
  } else {
712
1207
  console.log(`${hostsResult.hostsPath} already up to date`);
713
1208
  }
714
- const caddyfile = await writeCaddyfile(entries, options.cwd);
1209
+ const caddyfile = await writeCaddyfile(entries, options.cwd, { https });
715
1210
  await validateCaddyfile(caddyfile);
716
1211
  const statePath = writeLocalghostState(options.cwd, {
717
1212
  action: "setup",
@@ -722,18 +1217,63 @@ program.command("setup").description("Update /etc/hosts and generate/validate Ca
722
1217
  hostsChanged: hostsResult.changed,
723
1218
  ...hostsResult.tempPath ? { hostsTempPath: hostsResult.tempPath } : {},
724
1219
  caddyfilePath: caddyfile,
1220
+ caddyHttps: https,
1221
+ ...existingTrustMarkers(options.cwd),
725
1222
  entries
726
1223
  });
727
1224
  console.log(`Generated ${caddyfile}`);
1225
+ console.log(`Mode ${https ? "HTTPS" : "HTTP"}`);
728
1226
  console.log(`State ${statePath}`);
729
1227
  console.log("Setup complete.");
730
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
+ });
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) => {
1244
+ assertLocalDevelopment("reset");
1245
+ const projectName = sanitizeProjectName(options.project ?? getProjectName(options.cwd));
1246
+ const caddyfilePath = getCaddyfilePath(options.cwd);
1247
+ const statePath = getLocalghostStatePath(options.cwd);
1248
+ explainHostsPassword();
1249
+ const hostsResult = await removeSystemHosts(projectName);
1250
+ if (existsSync7(caddyfilePath)) {
1251
+ unlinkSync(caddyfilePath);
1252
+ console.log(`Removed ${caddyfilePath}`);
1253
+ } else {
1254
+ console.log(`${caddyfilePath} was not present`);
1255
+ }
1256
+ if (existsSync7(statePath)) {
1257
+ unlinkSync(statePath);
1258
+ console.log(`Removed ${statePath}`);
1259
+ } else {
1260
+ console.log(`${statePath} was not present`);
1261
+ }
1262
+ if (hostsResult.removed) {
1263
+ console.log(`Removed Localghost hosts block from ${hostsResult.hostsPath}`);
1264
+ } else {
1265
+ console.log(`No Localghost hosts block found in ${hostsResult.hostsPath}`);
1266
+ }
1267
+ console.log(".localghost was left in place. Run localghost setup when you are ready.");
1268
+ });
731
1269
  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) => {
1270
+ assertLocalDevelopment("teardown");
732
1271
  const projectName = sanitizeProjectName(options.project ?? getProjectName(options.cwd));
1272
+ explainHostsPassword();
733
1273
  const hostsResult = await removeSystemHosts(projectName);
734
1274
  const caddyfilePath = getCaddyfilePath(options.cwd);
735
1275
  let caddyfileRemoved = false;
736
- if (options.removeCaddyfile && existsSync5(caddyfilePath)) {
1276
+ if (options.removeCaddyfile && existsSync7(caddyfilePath)) {
737
1277
  unlinkSync(caddyfilePath);
738
1278
  caddyfileRemoved = true;
739
1279
  }
@@ -757,39 +1297,240 @@ program.command("teardown").description("Remove Localghost's managed /etc/hosts
757
1297
  }
758
1298
  console.log(`State ${statePath}`);
759
1299
  });
760
- program.command("status").description("Print Localghost's project-local state file").option("--cwd <path>", "Project directory", process.cwd()).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) => {
761
1301
  const state = readLocalghostState(options.cwd);
762
1302
  const statePath = getLocalghostStatePath(options.cwd);
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
+ });
1311
+ if (options.json) {
1312
+ console.log(JSON.stringify({ state, setup: readiness }, null, 2));
1313
+ return;
1314
+ }
763
1315
  if (!state) {
764
1316
  console.log(`No Localghost state found at ${statePath}`);
765
- return;
1317
+ } else {
1318
+ console.log(`State: ${statePath}`);
1319
+ console.log(`Last action: ${state.action}`);
1320
+ console.log(`Updated: ${state.updatedAt}`);
1321
+ console.log(`Project: ${state.projectName}`);
1322
+ if (state.configPath) console.log(`Config: ${state.configPath}`);
1323
+ if (state.hostsPath) console.log(`Hosts: ${state.hostsPath}`);
1324
+ if (state.caddyfilePath) console.log(`Caddyfile: ${state.caddyfilePath}`);
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})`);
1328
+ if (typeof state.caddyfileRemoved === "boolean") console.log(`Caddyfile removed: ${state.caddyfileRemoved}`);
766
1329
  }
767
- if (options.json) {
768
- console.log(JSON.stringify(state, null, 2));
1330
+ if (readiness.ready) {
1331
+ console.log("Setup ready: yes");
769
1332
  return;
770
1333
  }
771
- console.log(`State: ${statePath}`);
772
- console.log(`Last action: ${state.action}`);
773
- console.log(`Updated: ${state.updatedAt}`);
774
- console.log(`Project: ${state.projectName}`);
775
- if (state.configPath) console.log(`Config: ${state.configPath}`);
776
- if (state.hostsPath) console.log(`Hosts: ${state.hostsPath}`);
777
- if (state.caddyfilePath) console.log(`Caddyfile: ${state.caddyfilePath}`);
778
- if (typeof state.caddyfileRemoved === "boolean") console.log(`Caddyfile removed: ${state.caddyfileRemoved}`);
1334
+ console.log("Setup ready: no");
1335
+ for (const reason of readiness.reasons) {
1336
+ console.log(` - ${reason}`);
1337
+ }
1338
+ console.log(`Run: ${readiness.setupCommand}`);
1339
+ if (options.ready) {
1340
+ process.exitCode = 1;
1341
+ }
779
1342
  });
780
- 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").action((options) => {
781
- const entries = readDevHosts(readOptionsFromCli(options));
782
- warnAboutLocalMdns(entries);
783
- console.log(formatDomainRoutes(entries, { https: !options.http }));
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 }));
784
1347
  });
785
- program.command("dev").description("Generate Caddyfile and run Caddy").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(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) => {
1349
+ assertLocalDevelopment("dev");
786
1350
  await assertCaddyReady();
787
- const entries = readDevHosts(readOptionsFromCli(options));
788
- warnAboutLocalMdns(entries);
789
- logDomainRoutes(entries);
790
- const caddyfile = await writeCaddyfile(entries, options.cwd);
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
+ });
1360
+ if (!readiness.ready) {
1361
+ if (!options.setup) {
1362
+ throw new Error(
1363
+ [
1364
+ "Localghost setup is missing or stale.",
1365
+ ...readiness.reasons.map((reason) => `- ${reason}`),
1366
+ `Run: ${readiness.setupCommand}`,
1367
+ "Or rerun dev with --setup if you want Localghost to perform setup first."
1368
+ ].join("\n")
1369
+ );
1370
+ }
1371
+ explainHostsPassword();
1372
+ const hostsResult = await updateSystemHosts(readiness.projectName, readiness.entries);
1373
+ const caddyfilePath = await writeCaddyfile(readiness.entries, options.cwd, { https });
1374
+ await validateCaddyfile(caddyfilePath);
1375
+ writeLocalghostState(options.cwd, {
1376
+ action: "setup",
1377
+ projectName: readiness.projectName,
1378
+ cwd: options.cwd,
1379
+ configPath: readiness.configPath,
1380
+ hostsPath: hostsResult.hostsPath,
1381
+ hostsChanged: hostsResult.changed,
1382
+ ...hostsResult.tempPath ? { hostsTempPath: hostsResult.tempPath } : {},
1383
+ caddyfilePath,
1384
+ caddyHttps: https,
1385
+ ...existingTrustMarkers(options.cwd),
1386
+ entries: readiness.entries
1387
+ });
1388
+ }
1389
+ warnAboutLocalMdns(readiness.entries);
1390
+ logDomainRoutes(readiness.entries, { https });
1391
+ const caddyfile = await writeCaddyfile(readiness.entries, options.cwd, { https });
1392
+ await validateCaddyfile(caddyfile);
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
+ }
1405
+ const caddyPid = maybePid(caddy.pid);
1406
+ const runRecord = registerLocalghostRun({
1407
+ mode: "dev",
1408
+ cwd: options.cwd,
1409
+ projectName: readiness.projectName,
1410
+ configPath: readiness.configPath,
1411
+ caddyfilePath: caddyfile,
1412
+ ...caddyPid ? { caddyPid } : {},
1413
+ https,
1414
+ entries: readiness.entries
1415
+ });
1416
+ const cleanupRun = registerCleanup(runRecord.id);
1417
+ try {
1418
+ await caddy;
1419
+ } finally {
1420
+ cleanupRun();
1421
+ }
1422
+ });
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) => {
1424
+ assertLocalDevelopment("run");
1425
+ await assertCaddyReady();
1426
+ const context = await resolveLocalghostContext({
1427
+ cwd: options.cwd,
1428
+ ...options.project ? { project: options.project } : {},
1429
+ ...options.config && options.config.length > 0 ? { configFiles: options.config } : {},
1430
+ ...options.configPattern ? { configPattern: options.configPattern } : {},
1431
+ ...options.port ? { port: options.port } : {},
1432
+ ...useHttps(options) ? { https: true } : {},
1433
+ ...typeof options.dynamicPort === "boolean" ? { dynamicPort: options.dynamicPort } : {}
1434
+ });
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
+ });
1444
+ if (!readiness.ready) {
1445
+ const shouldSetup = options.setup === true || canPrompt() && await confirm("Run caddy:setup now?", true);
1446
+ if (!shouldSetup) {
1447
+ throw new Error(
1448
+ [
1449
+ "Localghost setup is missing or stale.",
1450
+ ...readiness.reasons.map((reason) => `- ${reason}`),
1451
+ `Run: ${readiness.setupCommand}`
1452
+ ].join("\n")
1453
+ );
1454
+ }
1455
+ await runSetupFromReadiness(options.cwd, https, readiness);
1456
+ console.log(`All set. Setup state: ${getLocalghostStatePath(options.cwd)}`);
1457
+ }
1458
+ if (context.dynamicPort && context.port !== context.requestedPort) {
1459
+ console.log(`Port ${context.requestedPort} is busy; using ${context.port}.`);
1460
+ }
1461
+ warnAboutLocalMdns(context.entries);
1462
+ logDomainRoutes(context.entries, { https });
1463
+ const caddyfile = await writeCaddyfile(context.entries, options.cwd, { https });
791
1464
  await validateCaddyfile(caddyfile);
792
- await runCaddy(caddyfile);
1465
+ const caddy = startCaddy(caddyfile);
1466
+ const caddyExit = caddy.catch((error) => {
1467
+ if (!caddy.killed) throw error;
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
+ }
1480
+ const [binary, ...args] = command;
1481
+ if (!binary) {
1482
+ throw new Error("Missing command. Use: localghost run -- vite");
1483
+ }
1484
+ const child = execa4(binary, args, {
1485
+ cwd: options.cwd,
1486
+ stdio: "inherit",
1487
+ env: {
1488
+ ...process.env,
1489
+ LOCALGHOST_PORT: String(context.port),
1490
+ LOCALGHOST_DYNAMIC_PORT: context.dynamicPort ? "1" : "0",
1491
+ VITE_PORT: String(context.port)
1492
+ }
1493
+ });
1494
+ const caddyPid = maybePid(caddy.pid);
1495
+ const childPid = maybePid(child.pid);
1496
+ const runRecord = registerLocalghostRun({
1497
+ mode: "run",
1498
+ cwd: context.cwd,
1499
+ projectName: context.projectName,
1500
+ configPath: context.configPath,
1501
+ caddyfilePath: caddyfile,
1502
+ ...caddyPid ? { caddyPid } : {},
1503
+ ...childPid ? { childPid } : {},
1504
+ childCommand: command,
1505
+ https,
1506
+ requestedPort: context.requestedPort,
1507
+ port: context.port,
1508
+ dynamicPort: context.dynamicPort,
1509
+ entries: context.entries
1510
+ });
1511
+ const cleanupRun = registerCleanup(runRecord.id);
1512
+ const stopCaddy = () => {
1513
+ if (!caddy.killed) caddy.kill("SIGINT");
1514
+ };
1515
+ const stopChild = () => {
1516
+ if (!child.killed) child.kill("SIGINT");
1517
+ };
1518
+ try {
1519
+ await Promise.race([child, caddyExit]);
1520
+ } finally {
1521
+ stopChild();
1522
+ stopCaddy();
1523
+ await Promise.allSettled([child, caddyExit]);
1524
+ cleanupRun();
1525
+ }
1526
+ });
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)));
1529
+ if (options.json) {
1530
+ console.log(JSON.stringify({ activityPath: getLocalghostActivityPath(), runs }, null, 2));
1531
+ return;
1532
+ }
1533
+ console.log(formatRunViews(runs));
793
1534
  });
794
1535
  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) => {
795
1536
  const entries = readDevHosts(readOptionsFromCli(options));