@hamedb89/localghost 0.1.3 → 0.1.6

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 existsSync6, 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,131 @@ 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),
178
274
  stdio: "inherit"
179
275
  });
180
276
  }
181
- async function runCaddy(path) {
182
- await execa("caddy", ["run", "--config", path], {
183
- cwd: dirname2(path),
277
+ function startCaddy(path) {
278
+ return execa("caddy", ["run", "--config", path], {
279
+ cwd: dirname3(path),
184
280
  stdio: "inherit"
185
281
  });
186
282
  }
187
283
 
284
+ // src/port.ts
285
+ import { createServer } from "net";
286
+ async function isPortAvailable(port, host = "127.0.0.1") {
287
+ return new Promise((resolve2) => {
288
+ const server = createServer();
289
+ server.once("error", () => {
290
+ resolve2(false);
291
+ });
292
+ server.once("listening", () => {
293
+ server.close(() => resolve2(true));
294
+ });
295
+ server.listen(port, host);
296
+ });
297
+ }
298
+ async function findAvailablePort(startPort, options = {}) {
299
+ const host = options.host ?? "127.0.0.1";
300
+ const maxAttempts = options.maxAttempts ?? 50;
301
+ for (let offset = 0; offset < maxAttempts; offset += 1) {
302
+ const port = startPort + offset;
303
+ if (await isPortAvailable(port, host)) {
304
+ return port;
305
+ }
306
+ }
307
+ throw new Error(`No available port found from ${startPort} to ${startPort + maxAttempts - 1}.`);
308
+ }
309
+
310
+ // src/context.ts
311
+ function parsePort(value) {
312
+ if (!value) return void 0;
313
+ const port = Number.parseInt(value, 10);
314
+ return Number.isInteger(port) && port > 0 && port <= 65535 ? port : void 0;
315
+ }
316
+ function envPort() {
317
+ return parsePort(process.env.LOCALGHOST_PORT) ?? parsePort(process.env.VITE_PORT);
318
+ }
319
+ function envDynamicPort() {
320
+ const value = process.env.LOCALGHOST_DYNAMIC_PORT;
321
+ if (!value) return void 0;
322
+ return ["1", "true", "yes", "on"].includes(value.toLowerCase());
323
+ }
324
+ function readOptionsFromContext(options) {
325
+ return {
326
+ cwd: options.cwd ?? process.cwd(),
327
+ ...options.fileName ? { fileName: options.fileName } : {},
328
+ ...options.configFiles ? { configFiles: options.configFiles } : {},
329
+ ...options.configPattern ? { configPattern: options.configPattern } : {}
330
+ };
331
+ }
332
+ function withRuntimePort(entries, requestedPort, port) {
333
+ if (requestedPort === port) return entries;
334
+ const hasRequestedPort = entries.some((entry) => entry.port === requestedPort);
335
+ if (!hasRequestedPort) return entries;
336
+ return entries.map((entry) => entry.port === requestedPort ? { ...entry, port } : entry);
337
+ }
338
+ function uniqueHosts(entries) {
339
+ return [...new Set(entries.map((entry) => entry.host))];
340
+ }
341
+ async function resolveLocalghostContext(options = {}) {
342
+ const cwd = options.cwd ?? process.cwd();
343
+ const readOptions = readOptionsFromContext({ ...options, cwd });
344
+ const resolvedPath = resolveDevHostsPath(readOptions);
345
+ 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";
349
+ const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
350
+ const port = dynamicPort ? await findAvailablePort(requestedPort, { host: probeHost }) : requestedPort;
351
+ const entries = withRuntimePort(configEntries, requestedPort, port);
352
+ const hosts = uniqueHosts(entries);
353
+ const primaryHost = options.primaryHost ?? entries.find((entry) => entry.port === port)?.host ?? hosts[0] ?? `${sanitizeProjectName(getProjectName(cwd))}.localhost`;
354
+ return {
355
+ cwd,
356
+ projectName: sanitizeProjectName(options.project ?? getProjectName(cwd)),
357
+ readOptions,
358
+ configPath: resolvedPath.path,
359
+ configFileName: resolvedPath.fileName,
360
+ configEntries,
361
+ entries,
362
+ hosts,
363
+ requestedPort,
364
+ port,
365
+ dynamicPort,
366
+ bindHost,
367
+ primaryHost,
368
+ https: options.https === true
369
+ };
370
+ }
371
+
188
372
  // src/doctor.ts
189
373
  import { execa as execa2 } from "execa";
190
374
  async function checkCaddy() {
@@ -211,10 +395,27 @@ async function runDoctor() {
211
395
  };
212
396
  }
213
397
 
398
+ // src/env.ts
399
+ function getProductionReason(env = process.env) {
400
+ if (env.LOCALGHOST_ENV === "production") return "LOCALGHOST_ENV=production";
401
+ if (env.NODE_ENV === "production") return "NODE_ENV=production";
402
+ if (env.VERCEL_ENV === "production") return "VERCEL_ENV=production";
403
+ if (env.NETLIFY === "true" && env.CONTEXT === "production") return "NETLIFY=true and CONTEXT=production";
404
+ if (env.CF_PAGES_BRANCH && env.CF_PAGES_BRANCH === env.CF_PAGES_PRODUCTION_BRANCH) {
405
+ return "CF_PAGES_BRANCH matches CF_PAGES_PRODUCTION_BRANCH";
406
+ }
407
+ return null;
408
+ }
409
+ function assertLocalDevelopment(command, env = process.env) {
410
+ const reason = getProductionReason(env);
411
+ if (!reason) return;
412
+ throw new Error(`Localghost only runs in local development. Refusing \`${command}\` because ${reason}.`);
413
+ }
414
+
214
415
  // src/hosts-file.ts
215
- import { writeFileSync as writeFileSync2 } from "fs";
416
+ import { writeFileSync as writeFileSync3 } from "fs";
216
417
  import { tmpdir } from "os";
217
- import { join as join3 } from "path";
418
+ import { join as join4 } from "path";
218
419
  import { execa as execa3 } from "execa";
219
420
  function escapeRegExp(value) {
220
421
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -256,8 +457,8 @@ function removeManagedBlock(existing, projectName) {
256
457
  }
257
458
  async function writeSystemHostsFile(hostsPath, next, projectName) {
258
459
  const sanitizedProjectName = sanitizeProjectName(projectName);
259
- const tempPath = join3(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
260
- writeFileSync2(tempPath, next, "utf8");
460
+ const tempPath = join4(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
461
+ writeFileSync3(tempPath, next, "utf8");
261
462
  if (process.platform === "win32") {
262
463
  throw new Error(`Windows support: run as administrator and copy ${tempPath} to ${hostsPath}.`);
263
464
  }
@@ -289,11 +490,11 @@ async function removeSystemHosts(projectName) {
289
490
  }
290
491
 
291
492
  // src/init.ts
292
- import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
293
- import { join as join4 } from "path";
493
+ import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
494
+ import { join as join5 } from "path";
294
495
  function detectPackageManager(cwd = process.cwd()) {
295
- if (existsSync2(join4(cwd, "pnpm-lock.yaml"))) return "pnpm";
296
- if (existsSync2(join4(cwd, "yarn.lock"))) return "yarn";
496
+ if (existsSync3(join5(cwd, "pnpm-lock.yaml"))) return "pnpm";
497
+ if (existsSync3(join5(cwd, "yarn.lock"))) return "yarn";
297
498
  return "npm";
298
499
  }
299
500
  function packageRunCommand(packageManager, script) {
@@ -313,7 +514,7 @@ function renderConfig(options) {
313
514
  }
314
515
  function readPackageJson(path) {
315
516
  try {
316
- return JSON.parse(readFileSync3(path, "utf8"));
517
+ return JSON.parse(readFileSync4(path, "utf8"));
317
518
  } catch {
318
519
  return null;
319
520
  }
@@ -334,17 +535,24 @@ function updatePackageScripts(packageJsonPath, configFile) {
334
535
  ...scripts,
335
536
  "localghost:setup": scripts["localghost:setup"] ?? `localghost setup${configFlag}`,
336
537
  "localghost:proxy": scripts["localghost:proxy"] ?? `localghost dev${configFlag}`,
538
+ "localghost:proxy:https": scripts["localghost:proxy:https"] ?? `localghost dev${configFlag} --https`,
539
+ "localghost:run": scripts["localghost:run"] ?? `localghost run${configFlag} --`,
540
+ "localghost:ready": scripts["localghost:ready"] ?? `localghost status${configFlag} --ready`,
541
+ "localghost:ps": scripts["localghost:ps"] ?? "localghost ps",
337
542
  "localghost:print": scripts["localghost:print"] ?? `localghost print${configFlag}`,
338
543
  "localghost:routes": scripts["localghost:routes"] ?? `localghost routes${configFlag}`,
339
544
  "localghost:status": scripts["localghost:status"] ?? "localghost status",
545
+ "localghost:reset": scripts["localghost:reset"] ?? "localghost reset",
340
546
  "localghost:teardown": scripts["localghost:teardown"] ?? "localghost teardown",
341
547
  "localghost:doctor": scripts["localghost:doctor"] ?? "localghost doctor",
342
- "localghost:update": scripts["localghost:update"] ?? "localghost update"
548
+ "localghost:update": scripts["localghost:update"] ?? "localghost update",
549
+ "caddy:setup": scripts["caddy:setup"] ?? `localghost setup${configFlag}`,
550
+ "caddy:dev": scripts["caddy:dev"] ?? `localghost dev${configFlag}`
343
551
  };
344
552
  const changed = JSON.stringify(scripts) !== JSON.stringify(nextScripts);
345
553
  if (!changed) return false;
346
554
  pkg.scripts = nextScripts;
347
- writeFileSync3(packageJsonPath, `${JSON.stringify(pkg, null, 2)}
555
+ writeFileSync4(packageJsonPath, `${JSON.stringify(pkg, null, 2)}
348
556
  `, "utf8");
349
557
  return true;
350
558
  }
@@ -357,8 +565,8 @@ function initLocalghost(options = {}) {
357
565
  const apiPort = options.apiPort ?? 8787;
358
566
  const packageManager = options.packageManager ?? detectPackageManager(cwd);
359
567
  const configFile = options.configFile ?? LOCALGHOST_CONFIG_FILE;
360
- const configPath = join4(cwd, configFile);
361
- const configExists = existsSync2(configPath);
568
+ const configPath = join5(cwd, configFile);
569
+ const configExists = existsSync3(configPath);
362
570
  if (configExists && !options.force) {
363
571
  return {
364
572
  configPath,
@@ -368,30 +576,55 @@ function initLocalghost(options = {}) {
368
576
  nextSteps: [
369
577
  packageRunCommand(packageManager, "localghost:doctor"),
370
578
  packageRunCommand(packageManager, "localghost:setup"),
579
+ packageRunCommand(packageManager, "localghost:ready"),
371
580
  packageRunCommand(packageManager, "localghost:proxy")
372
581
  ]
373
582
  };
374
583
  }
375
584
  writeTextFile(configPath, renderConfig({ host, port, apiHost, apiPort }));
376
- const packageJsonPath = join4(cwd, "package.json");
585
+ const packageJsonPath = join5(cwd, "package.json");
377
586
  const packageJsonChanged = options.writeScripts ? updatePackageScripts(packageJsonPath, configFile) : false;
378
587
  return {
379
588
  configPath,
380
589
  configCreated: true,
381
- ...existsSync2(packageJsonPath) ? { packageJsonPath } : {},
590
+ ...existsSync3(packageJsonPath) ? { packageJsonPath } : {},
382
591
  packageJsonChanged,
383
592
  packageManager,
384
593
  nextSteps: [
385
594
  packageRunCommand(packageManager, "localghost:doctor"),
386
595
  packageRunCommand(packageManager, "localghost:setup"),
596
+ packageRunCommand(packageManager, "localghost:ready"),
387
597
  packageRunCommand(packageManager, "localghost:proxy")
388
598
  ]
389
599
  };
390
600
  }
391
601
 
602
+ // src/prompt.ts
603
+ import { stdin as input, stdout as output } from "process";
604
+ import { createInterface } from "readline/promises";
605
+ function canPrompt() {
606
+ return Boolean(input.isTTY && output.isTTY);
607
+ }
608
+ async function withPrompt(run) {
609
+ const rl = createInterface({ input, output });
610
+ try {
611
+ return await run((question) => rl.question(question));
612
+ } finally {
613
+ rl.close();
614
+ }
615
+ }
616
+ async function confirm(question, defaultValue = true) {
617
+ return withPrompt(async (prompt) => {
618
+ const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
619
+ const answer = (await prompt(`${question}${suffix}`)).trim().toLowerCase();
620
+ if (!answer) return defaultValue;
621
+ return answer === "y" || answer === "yes";
622
+ });
623
+ }
624
+
392
625
  // src/routes.ts
393
626
  function getDomainRoutes(entries, options = {}) {
394
- const protocol = options.https === false ? "http" : "https";
627
+ const protocol = options.https === true ? "https" : "http";
395
628
  return [...entries].sort((left, right) => left.host.localeCompare(right.host) || left.port - right.port).map((entry) => ({
396
629
  host: entry.host,
397
630
  port: entry.port,
@@ -411,15 +644,15 @@ function formatDomainRoutes(entries, options = {}) {
411
644
  }
412
645
 
413
646
  // src/state.ts
414
- import { existsSync as existsSync3 } from "fs";
415
- import { join as join5 } from "path";
647
+ import { existsSync as existsSync4 } from "fs";
648
+ import { join as join6 } from "path";
416
649
  var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
417
650
  function getLocalghostStatePath(cwd = process.cwd()) {
418
- return join5(cwd, LOCALGHOST_STATE_FILE);
651
+ return join6(cwd, LOCALGHOST_STATE_FILE);
419
652
  }
420
653
  function readLocalghostState(cwd = process.cwd()) {
421
654
  const path = getLocalghostStatePath(cwd);
422
- if (!existsSync3(path)) return null;
655
+ if (!existsSync4(path)) return null;
423
656
  return JSON.parse(readTextFile(path));
424
657
  }
425
658
  function writeLocalghostState(cwd, state) {
@@ -430,11 +663,11 @@ function writeLocalghostState(cwd, state) {
430
663
  }
431
664
 
432
665
  // 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";
666
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
667
+ import { homedir as homedir2 } from "os";
668
+ import { dirname as dirname4, join as join7 } from "path";
436
669
  var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
437
- var LOCALGHOST_VERSION = "0.1.0";
670
+ var LOCALGHOST_VERSION = "0.1.6";
438
671
  var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
439
672
  var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
440
673
  var UPDATE_CHECK_TIMEOUT_MS = 900;
@@ -446,21 +679,21 @@ function isUpdateCheckDisabled(env = process.env) {
446
679
  }
447
680
  function getUpdateCheckCachePath(env = process.env) {
448
681
  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");
682
+ const cacheRoot = env.XDG_CACHE_HOME || join7(homedir2(), ".cache");
683
+ return join7(cacheRoot, "localghost", "update-check.json");
451
684
  }
452
685
  function readCache(path = getUpdateCheckCachePath()) {
453
- if (!existsSync4(path)) return null;
686
+ if (!existsSync5(path)) return null;
454
687
  try {
455
- return JSON.parse(readFileSync4(path, "utf8"));
688
+ return JSON.parse(readFileSync5(path, "utf8"));
456
689
  } catch {
457
690
  return null;
458
691
  }
459
692
  }
460
693
  function writeCache(cache, path = getUpdateCheckCachePath()) {
461
694
  try {
462
- mkdirSync2(dirname3(path), { recursive: true });
463
- writeFileSync4(path, `${JSON.stringify(cache, null, 2)}
695
+ mkdirSync3(dirname4(path), { recursive: true });
696
+ writeFileSync5(path, `${JSON.stringify(cache, null, 2)}
464
697
  `, "utf8");
465
698
  } catch {
466
699
  }
@@ -597,6 +830,7 @@ ${message}`);
597
830
  }
598
831
 
599
832
  // src/cli.ts
833
+ import { execa as execa4 } from "execa";
600
834
  function warnAboutLocalMdns(entries) {
601
835
  const localHosts = findLocalMdnsHosts(entries);
602
836
  if (localHosts.length > 0) {
@@ -605,10 +839,10 @@ function warnAboutLocalMdns(entries) {
605
839
  );
606
840
  }
607
841
  }
608
- function logDomainRoutes(entries) {
609
- console.log(formatDomainRoutes(entries));
842
+ function logDomainRoutes(entries, options = {}) {
843
+ console.log(formatDomainRoutes(entries, options));
610
844
  }
611
- function parsePort(value) {
845
+ function parsePort2(value) {
612
846
  const port = Number.parseInt(value, 10);
613
847
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
614
848
  throw new InvalidArgumentError("Port must be a number between 1 and 65535.");
@@ -622,6 +856,14 @@ function parsePackageManager(value) {
622
856
  function collect(value, previous = []) {
623
857
  return [...previous, value];
624
858
  }
859
+ function parseBooleanLike(value) {
860
+ if (value === true) return true;
861
+ if (value === false) return false;
862
+ const normalized = value.toLowerCase();
863
+ if (["1", "true", "yes", "y", "on"].includes(normalized)) return true;
864
+ if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
865
+ throw new InvalidArgumentError("Value must be yes or no.");
866
+ }
625
867
  function readOptionsFromCli(options) {
626
868
  return {
627
869
  cwd: options.cwd,
@@ -638,6 +880,138 @@ async function assertCaddyReady() {
638
880
  "Localghost will not install it for you. No surprise spells."
639
881
  ].join("\n"));
640
882
  }
883
+ function explainHostsPassword() {
884
+ console.log("Localghost may ask for your password to update its managed block in /etc/hosts.");
885
+ console.log("It will only touch the lines between # localghost:start and # localghost:end.");
886
+ }
887
+ function useHttps(options) {
888
+ return options.https === true || options.ssl === true;
889
+ }
890
+ function getSetupCommand(options) {
891
+ const configFlags = [
892
+ ...(options.config ?? []).map((config) => ` --config ${config}`),
893
+ ...options.configPattern ? [` --config-pattern ${options.configPattern}`] : []
894
+ ].join("");
895
+ return `localghost setup${configFlags}${options.https ? " --https" : ""}`;
896
+ }
897
+ function getSetupReadiness(options) {
898
+ const projectName = sanitizeProjectName(options.project ?? getProjectName(options.cwd));
899
+ const readOptions = readOptionsFromCli(options);
900
+ const entries = readDevHosts(readOptions);
901
+ const configPath = resolveDevHostsPath(readOptions).path;
902
+ const caddyfilePath = getCaddyfilePath(options.cwd);
903
+ const statePath = getLocalghostStatePath(options.cwd);
904
+ const state = readLocalghostState(options.cwd);
905
+ const https = options.https === true;
906
+ const reasons = [];
907
+ if (!state) {
908
+ reasons.push(`No Localghost setup state found at ${statePath}.`);
909
+ } else {
910
+ if (state.action !== "setup") reasons.push(`Last Localghost action is ${state.action}, not setup.`);
911
+ if (state.projectName !== projectName) reasons.push(`Setup state is for project ${state.projectName}, not ${projectName}.`);
912
+ if (state.configPath !== configPath) reasons.push(`Setup state points at ${state.configPath ?? "no config"}, not ${configPath}.`);
913
+ }
914
+ const hostsPath = getSystemHostsPath();
915
+ try {
916
+ const hosts = readFileSync6(hostsPath, "utf8");
917
+ const expectedHostsBlock = renderHostsBlock(projectName, entries).trimEnd();
918
+ if (!hosts.includes(expectedHostsBlock)) {
919
+ reasons.push(`The Localghost hosts block in ${hostsPath} is missing or stale.`);
920
+ }
921
+ } catch (error) {
922
+ const message = error instanceof Error ? error.message : String(error);
923
+ reasons.push(`Could not read ${hostsPath}: ${message}`);
924
+ }
925
+ if (!options.ignoreCaddyfile) {
926
+ if (!existsSync6(caddyfilePath)) {
927
+ reasons.push(`Missing Caddyfile at ${caddyfilePath}.`);
928
+ } else {
929
+ const expectedCaddyfile = renderCaddyfile(entries, { https });
930
+ const currentCaddyfile = readFileSync6(caddyfilePath, "utf8");
931
+ if (currentCaddyfile !== expectedCaddyfile) {
932
+ reasons.push(`Caddyfile at ${caddyfilePath} is stale for ${https ? "HTTPS" : "HTTP"} mode.`);
933
+ }
934
+ }
935
+ }
936
+ return {
937
+ ready: reasons.length === 0,
938
+ reasons,
939
+ entries,
940
+ projectName,
941
+ configPath,
942
+ caddyfilePath,
943
+ statePath,
944
+ setupCommand: getSetupCommand(options)
945
+ };
946
+ }
947
+ async function runSetupFromReadiness(cwd, https, readiness) {
948
+ explainHostsPassword();
949
+ const hostsResult = await updateSystemHosts(readiness.projectName, readiness.entries);
950
+ const caddyfilePath = await writeCaddyfile(readiness.entries, cwd, { https });
951
+ await validateCaddyfile(caddyfilePath);
952
+ writeLocalghostState(cwd, {
953
+ action: "setup",
954
+ projectName: readiness.projectName,
955
+ cwd,
956
+ configPath: readiness.configPath,
957
+ hostsPath: hostsResult.hostsPath,
958
+ hostsChanged: hostsResult.changed,
959
+ ...hostsResult.tempPath ? { hostsTempPath: hostsResult.tempPath } : {},
960
+ caddyfilePath,
961
+ caddyHttps: https,
962
+ entries: readiness.entries
963
+ });
964
+ }
965
+ function maybePid(pid) {
966
+ return typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? pid : void 0;
967
+ }
968
+ function registerCleanup(id) {
969
+ let cleaned = false;
970
+ const cleanup = () => {
971
+ if (cleaned) return;
972
+ cleaned = true;
973
+ unregisterLocalghostRun(id);
974
+ };
975
+ process.once("exit", cleanup);
976
+ return () => {
977
+ cleanup();
978
+ process.off("exit", cleanup);
979
+ };
980
+ }
981
+ async function getRunView(run) {
982
+ const portStatus = /* @__PURE__ */ new Map();
983
+ for (const entry of run.entries) {
984
+ if (!portStatus.has(entry.port)) {
985
+ portStatus.set(entry.port, !await isPortAvailable(entry.port));
986
+ }
987
+ }
988
+ return {
989
+ ...run,
990
+ routes: run.entries.map((entry) => ({
991
+ host: entry.host,
992
+ port: entry.port,
993
+ target: `127.0.0.1:${entry.port}`,
994
+ listening: portStatus.get(entry.port) ?? false
995
+ }))
996
+ };
997
+ }
998
+ function formatRunViews(runs) {
999
+ if (runs.length === 0) return "No Localghost apps are running.";
1000
+ const lines = ["localghost ps"];
1001
+ for (const run of runs) {
1002
+ const command = run.childCommand?.length ? ` ${run.childCommand.join(" ")}` : "";
1003
+ const mode = command ? `${run.mode}:${command}` : run.mode;
1004
+ lines.push("");
1005
+ lines.push(`${run.projectName} ${mode}`);
1006
+ lines.push(` cwd: ${run.cwd}`);
1007
+ lines.push(` pid: ${run.pid}${run.caddyPid ? `, caddy: ${run.caddyPid}` : ""}${run.childPid ? `, child: ${run.childPid}` : ""}`);
1008
+ lines.push(` started: ${run.startedAt}`);
1009
+ for (const route of run.routes) {
1010
+ lines.push(` ${route.host} -> ${route.target} (${route.listening ? "listening" : "not listening"})`);
1011
+ }
1012
+ }
1013
+ return lines.join("\n");
1014
+ }
641
1015
  var program = new Command();
642
1016
  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
1017
  program.hook("postAction", async (_thisCommand, actionCommand) => {
@@ -645,7 +1019,7 @@ program.hook("postAction", async (_thisCommand, actionCommand) => {
645
1019
  const options = program.opts();
646
1020
  await maybeNotifyAboutUpdate({ disabled: options.updateCheck === false });
647
1021
  });
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) => {
1022
+ 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
1023
  const result = initLocalghost({ ...options, configFile: options.config });
650
1024
  if (result.configCreated) {
651
1025
  console.log(`Buh. Created ${result.configPath}`);
@@ -697,21 +1071,24 @@ program.command("update").description("Check npm for a newer localghost release"
697
1071
  }
698
1072
  console.log(`localghost is up to date. Current: ${result.currentVersion}`);
699
1073
  });
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) => {
1074
+ 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
+ assertLocalDevelopment("setup");
701
1076
  await assertCaddyReady();
1077
+ const https = useHttps(options);
702
1078
  const projectName = sanitizeProjectName(options.project ?? getProjectName(options.cwd));
703
1079
  const readOptions = readOptionsFromCli(options);
704
1080
  const configPath = resolveDevHostsPath(readOptions).path;
705
1081
  const entries = readDevHosts(readOptions);
706
1082
  warnAboutLocalMdns(entries);
707
- logDomainRoutes(entries);
1083
+ logDomainRoutes(entries, { https });
1084
+ explainHostsPassword();
708
1085
  const hostsResult = await updateSystemHosts(projectName, entries);
709
1086
  if (hostsResult.changed) {
710
1087
  console.log(`Updated ${hostsResult.hostsPath}`);
711
1088
  } else {
712
1089
  console.log(`${hostsResult.hostsPath} already up to date`);
713
1090
  }
714
- const caddyfile = await writeCaddyfile(entries, options.cwd);
1091
+ const caddyfile = await writeCaddyfile(entries, options.cwd, { https });
715
1092
  await validateCaddyfile(caddyfile);
716
1093
  const statePath = writeLocalghostState(options.cwd, {
717
1094
  action: "setup",
@@ -722,18 +1099,48 @@ program.command("setup").description("Update /etc/hosts and generate/validate Ca
722
1099
  hostsChanged: hostsResult.changed,
723
1100
  ...hostsResult.tempPath ? { hostsTempPath: hostsResult.tempPath } : {},
724
1101
  caddyfilePath: caddyfile,
1102
+ caddyHttps: https,
725
1103
  entries
726
1104
  });
727
1105
  console.log(`Generated ${caddyfile}`);
1106
+ console.log(`Mode ${https ? "HTTPS" : "HTTP"}`);
728
1107
  console.log(`State ${statePath}`);
729
1108
  console.log("Setup complete.");
730
1109
  });
1110
+ 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
+ assertLocalDevelopment("reset");
1112
+ const projectName = sanitizeProjectName(options.project ?? getProjectName(options.cwd));
1113
+ const caddyfilePath = getCaddyfilePath(options.cwd);
1114
+ const statePath = getLocalghostStatePath(options.cwd);
1115
+ explainHostsPassword();
1116
+ const hostsResult = await removeSystemHosts(projectName);
1117
+ if (existsSync6(caddyfilePath)) {
1118
+ unlinkSync(caddyfilePath);
1119
+ console.log(`Removed ${caddyfilePath}`);
1120
+ } else {
1121
+ console.log(`${caddyfilePath} was not present`);
1122
+ }
1123
+ if (existsSync6(statePath)) {
1124
+ unlinkSync(statePath);
1125
+ console.log(`Removed ${statePath}`);
1126
+ } else {
1127
+ console.log(`${statePath} was not present`);
1128
+ }
1129
+ if (hostsResult.removed) {
1130
+ console.log(`Removed Localghost hosts block from ${hostsResult.hostsPath}`);
1131
+ } else {
1132
+ console.log(`No Localghost hosts block found in ${hostsResult.hostsPath}`);
1133
+ }
1134
+ console.log(".localghost was left in place. Run localghost setup when you are ready.");
1135
+ });
731
1136
  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) => {
1137
+ assertLocalDevelopment("teardown");
732
1138
  const projectName = sanitizeProjectName(options.project ?? getProjectName(options.cwd));
1139
+ explainHostsPassword();
733
1140
  const hostsResult = await removeSystemHosts(projectName);
734
1141
  const caddyfilePath = getCaddyfilePath(options.cwd);
735
1142
  let caddyfileRemoved = false;
736
- if (options.removeCaddyfile && existsSync5(caddyfilePath)) {
1143
+ if (options.removeCaddyfile && existsSync6(caddyfilePath)) {
737
1144
  unlinkSync(caddyfilePath);
738
1145
  caddyfileRemoved = true;
739
1146
  }
@@ -757,39 +1164,194 @@ program.command("teardown").description("Remove Localghost's managed /etc/hosts
757
1164
  }
758
1165
  console.log(`State ${statePath}`);
759
1166
  });
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) => {
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) => {
761
1168
  const state = readLocalghostState(options.cwd);
762
1169
  const statePath = getLocalghostStatePath(options.cwd);
1170
+ const readiness = getSetupReadiness({ ...options, https: useHttps(options) });
1171
+ if (options.json) {
1172
+ console.log(JSON.stringify({ state, setup: readiness }, null, 2));
1173
+ return;
1174
+ }
763
1175
  if (!state) {
764
1176
  console.log(`No Localghost state found at ${statePath}`);
765
- return;
1177
+ } else {
1178
+ console.log(`State: ${statePath}`);
1179
+ console.log(`Last action: ${state.action}`);
1180
+ console.log(`Updated: ${state.updatedAt}`);
1181
+ console.log(`Project: ${state.projectName}`);
1182
+ if (state.configPath) console.log(`Config: ${state.configPath}`);
1183
+ if (state.hostsPath) console.log(`Hosts: ${state.hostsPath}`);
1184
+ if (state.caddyfilePath) console.log(`Caddyfile: ${state.caddyfilePath}`);
1185
+ if (typeof state.caddyHttps === "boolean") console.log(`Mode: ${state.caddyHttps ? "HTTPS" : "HTTP"}`);
1186
+ if (typeof state.caddyfileRemoved === "boolean") console.log(`Caddyfile removed: ${state.caddyfileRemoved}`);
766
1187
  }
767
- if (options.json) {
768
- console.log(JSON.stringify(state, null, 2));
1188
+ if (readiness.ready) {
1189
+ console.log("Setup ready: yes");
769
1190
  return;
770
1191
  }
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}`);
1192
+ console.log("Setup ready: no");
1193
+ for (const reason of readiness.reasons) {
1194
+ console.log(` - ${reason}`);
1195
+ }
1196
+ console.log(`Run: ${readiness.setupCommand}`);
1197
+ if (options.ready) {
1198
+ process.exitCode = 1;
1199
+ }
779
1200
  });
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) => {
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) => {
781
1202
  const entries = readDevHosts(readOptionsFromCli(options));
782
1203
  warnAboutLocalMdns(entries);
783
- console.log(formatDomainRoutes(entries, { https: !options.http }));
1204
+ console.log(formatDomainRoutes(entries, { https: options.http ? false : useHttps(options) }));
784
1205
  });
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) => {
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) => {
1207
+ assertLocalDevelopment("dev");
786
1208
  await assertCaddyReady();
787
- const entries = readDevHosts(readOptionsFromCli(options));
788
- warnAboutLocalMdns(entries);
789
- logDomainRoutes(entries);
790
- const caddyfile = await writeCaddyfile(entries, options.cwd);
1209
+ const https = useHttps(options);
1210
+ const readiness = getSetupReadiness({ ...options, https });
1211
+ if (!readiness.ready) {
1212
+ if (!options.setup) {
1213
+ throw new Error(
1214
+ [
1215
+ "Localghost setup is missing or stale.",
1216
+ ...readiness.reasons.map((reason) => `- ${reason}`),
1217
+ `Run: ${readiness.setupCommand}`,
1218
+ "Or rerun dev with --setup if you want Localghost to perform setup first."
1219
+ ].join("\n")
1220
+ );
1221
+ }
1222
+ explainHostsPassword();
1223
+ const hostsResult = await updateSystemHosts(readiness.projectName, readiness.entries);
1224
+ const caddyfilePath = await writeCaddyfile(readiness.entries, options.cwd, { https });
1225
+ await validateCaddyfile(caddyfilePath);
1226
+ writeLocalghostState(options.cwd, {
1227
+ action: "setup",
1228
+ projectName: readiness.projectName,
1229
+ cwd: options.cwd,
1230
+ configPath: readiness.configPath,
1231
+ hostsPath: hostsResult.hostsPath,
1232
+ hostsChanged: hostsResult.changed,
1233
+ ...hostsResult.tempPath ? { hostsTempPath: hostsResult.tempPath } : {},
1234
+ caddyfilePath,
1235
+ caddyHttps: https,
1236
+ entries: readiness.entries
1237
+ });
1238
+ }
1239
+ warnAboutLocalMdns(readiness.entries);
1240
+ logDomainRoutes(readiness.entries, { https });
1241
+ const caddyfile = await writeCaddyfile(readiness.entries, options.cwd, { https });
791
1242
  await validateCaddyfile(caddyfile);
792
- await runCaddy(caddyfile);
1243
+ const caddy = startCaddy(caddyfile);
1244
+ const caddyPid = maybePid(caddy.pid);
1245
+ const runRecord = registerLocalghostRun({
1246
+ mode: "dev",
1247
+ cwd: options.cwd,
1248
+ projectName: readiness.projectName,
1249
+ configPath: readiness.configPath,
1250
+ caddyfilePath: caddyfile,
1251
+ ...caddyPid ? { caddyPid } : {},
1252
+ https,
1253
+ entries: readiness.entries
1254
+ });
1255
+ const cleanupRun = registerCleanup(runRecord.id);
1256
+ try {
1257
+ await caddy;
1258
+ } finally {
1259
+ cleanupRun();
1260
+ }
1261
+ });
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) => {
1263
+ assertLocalDevelopment("run");
1264
+ await assertCaddyReady();
1265
+ const https = useHttps(options);
1266
+ const context = await resolveLocalghostContext({
1267
+ cwd: options.cwd,
1268
+ ...options.project ? { project: options.project } : {},
1269
+ ...options.config && options.config.length > 0 ? { configFiles: options.config } : {},
1270
+ ...options.configPattern ? { configPattern: options.configPattern } : {},
1271
+ ...options.port ? { port: options.port } : {},
1272
+ https,
1273
+ ...typeof options.dynamicPort === "boolean" ? { dynamicPort: options.dynamicPort } : {}
1274
+ });
1275
+ const readiness = getSetupReadiness({ ...options, https, ignoreCaddyfile: true });
1276
+ if (!readiness.ready) {
1277
+ const shouldSetup = options.setup === true || canPrompt() && await confirm("Run caddy:setup now?", true);
1278
+ if (!shouldSetup) {
1279
+ throw new Error(
1280
+ [
1281
+ "Localghost setup is missing or stale.",
1282
+ ...readiness.reasons.map((reason) => `- ${reason}`),
1283
+ `Run: ${readiness.setupCommand}`
1284
+ ].join("\n")
1285
+ );
1286
+ }
1287
+ await runSetupFromReadiness(options.cwd, https, readiness);
1288
+ console.log(`All set. Setup state: ${getLocalghostStatePath(options.cwd)}`);
1289
+ }
1290
+ if (context.dynamicPort && context.port !== context.requestedPort) {
1291
+ console.log(`Port ${context.requestedPort} is busy; using ${context.port}.`);
1292
+ }
1293
+ warnAboutLocalMdns(context.entries);
1294
+ logDomainRoutes(context.entries, { https });
1295
+ const caddyfile = await writeCaddyfile(context.entries, options.cwd, { https });
1296
+ await validateCaddyfile(caddyfile);
1297
+ const caddy = startCaddy(caddyfile);
1298
+ const caddyExit = caddy.catch((error) => {
1299
+ if (!caddy.killed) throw error;
1300
+ });
1301
+ const [binary, ...args] = command;
1302
+ if (!binary) {
1303
+ throw new Error("Missing command. Use: localghost run -- vite");
1304
+ }
1305
+ const child = execa4(binary, args, {
1306
+ cwd: options.cwd,
1307
+ stdio: "inherit",
1308
+ env: {
1309
+ ...process.env,
1310
+ LOCALGHOST_PORT: String(context.port),
1311
+ LOCALGHOST_DYNAMIC_PORT: context.dynamicPort ? "1" : "0",
1312
+ VITE_PORT: String(context.port)
1313
+ }
1314
+ });
1315
+ const caddyPid = maybePid(caddy.pid);
1316
+ const childPid = maybePid(child.pid);
1317
+ const runRecord = registerLocalghostRun({
1318
+ mode: "run",
1319
+ cwd: context.cwd,
1320
+ projectName: context.projectName,
1321
+ configPath: context.configPath,
1322
+ caddyfilePath: caddyfile,
1323
+ ...caddyPid ? { caddyPid } : {},
1324
+ ...childPid ? { childPid } : {},
1325
+ childCommand: command,
1326
+ https,
1327
+ requestedPort: context.requestedPort,
1328
+ port: context.port,
1329
+ dynamicPort: context.dynamicPort,
1330
+ entries: context.entries
1331
+ });
1332
+ const cleanupRun = registerCleanup(runRecord.id);
1333
+ const stopCaddy = () => {
1334
+ if (!caddy.killed) caddy.kill("SIGINT");
1335
+ };
1336
+ const stopChild = () => {
1337
+ if (!child.killed) child.kill("SIGINT");
1338
+ };
1339
+ try {
1340
+ await Promise.race([child, caddyExit]);
1341
+ } finally {
1342
+ stopChild();
1343
+ stopCaddy();
1344
+ await Promise.allSettled([child, caddyExit]);
1345
+ cleanupRun();
1346
+ }
1347
+ });
1348
+ program.command("ps").description("Show Localghost dev sessions that are currently running").option("--json", "Print raw JSON").action(async (options) => {
1349
+ const runs = await Promise.all(listLocalghostRuns().map((run) => getRunView(run)));
1350
+ if (options.json) {
1351
+ console.log(JSON.stringify({ activityPath: getLocalghostActivityPath(), runs }, null, 2));
1352
+ return;
1353
+ }
1354
+ console.log(formatRunViews(runs));
793
1355
  });
794
1356
  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
1357
  const entries = readDevHosts(readOptionsFromCli(options));