@hamedb89/localghost 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,648 @@
1
+ // src/config.ts
2
+ import { existsSync, readFileSync, readdirSync } from "fs";
3
+ import { basename, join, resolve } from "path";
4
+
5
+ // src/parse.ts
6
+ var HOST_PATTERN = /^(?=.{1,253}$)(?!-)[a-z0-9-]+(\.[a-z0-9-]+)*\.?$/i;
7
+ function parseDevHosts(input, fileName = ".localghost") {
8
+ const entries = [];
9
+ input.split(/\r?\n/).forEach((rawLine, index) => {
10
+ const line = rawLine.replace(/#.*/, "").trim();
11
+ if (!line) {
12
+ return;
13
+ }
14
+ const parts = line.split(/\s+/);
15
+ const host = parts[0];
16
+ const portRaw = parts[1];
17
+ if (!host || !portRaw || parts.length > 2) {
18
+ throw new Error(`Invalid ${fileName} line ${index + 1}: "${rawLine}"`);
19
+ }
20
+ if (!HOST_PATTERN.test(host)) {
21
+ throw new Error(`Invalid host on line ${index + 1}: "${host}"`);
22
+ }
23
+ const port = Number(portRaw);
24
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
25
+ throw new Error(`Invalid port on line ${index + 1}: "${portRaw}"`);
26
+ }
27
+ entries.push({
28
+ host: host.toLowerCase().replace(/\.$/, ""),
29
+ port,
30
+ target: `127.0.0.1:${port}`
31
+ });
32
+ });
33
+ return entries;
34
+ }
35
+ function findLocalMdnsHosts(entries) {
36
+ return [...new Set(entries.map((entry) => entry.host).filter((host) => host.endsWith(".local")))];
37
+ }
38
+
39
+ // src/config.ts
40
+ var LOCALGHOST_CONFIG_FILE = ".localghost";
41
+ function unique(values) {
42
+ return [...new Set(values.filter(Boolean))];
43
+ }
44
+ function toRegExp(pattern) {
45
+ return typeof pattern === "string" ? new RegExp(pattern) : pattern;
46
+ }
47
+ function findPatternMatches(cwd, pattern) {
48
+ const matcher = toRegExp(pattern);
49
+ return readdirSync(cwd, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => entry.name).filter((name) => {
50
+ matcher.lastIndex = 0;
51
+ return matcher.test(name);
52
+ }).sort();
53
+ }
54
+ function getConfigFileCandidates(options = {}) {
55
+ const cwd = options.cwd ?? process.cwd();
56
+ const exactFiles = unique([
57
+ ...options.fileName ? [options.fileName] : [],
58
+ ...options.configFiles ?? []
59
+ ]);
60
+ const patternFiles = options.configPattern ? findPatternMatches(cwd, options.configPattern) : [];
61
+ const candidates = unique([...exactFiles, ...patternFiles]);
62
+ if (candidates.length > 0) return candidates;
63
+ if (exactFiles.length > 0 || options.configPattern) return [];
64
+ return [LOCALGHOST_CONFIG_FILE];
65
+ }
66
+ function resolveDevHostsPath(options = {}) {
67
+ const cwd = options.cwd ?? process.cwd();
68
+ const searchedFiles = getConfigFileCandidates(options);
69
+ for (const fileName2 of searchedFiles) {
70
+ const path = resolve(cwd, fileName2);
71
+ if (existsSync(path)) {
72
+ return {
73
+ path,
74
+ fileName: basename(fileName2),
75
+ exists: true,
76
+ searchedFiles,
77
+ ...options.configPattern ? { configPattern: options.configPattern } : {}
78
+ };
79
+ }
80
+ }
81
+ const fileName = searchedFiles[0] ?? LOCALGHOST_CONFIG_FILE;
82
+ return {
83
+ path: resolve(cwd, fileName),
84
+ fileName: basename(fileName),
85
+ exists: false,
86
+ searchedFiles,
87
+ ...options.configPattern ? { configPattern: options.configPattern } : {}
88
+ };
89
+ }
90
+ function getDevHostsPath(options = {}) {
91
+ return resolveDevHostsPath(options).path;
92
+ }
93
+ function formatSearchedFiles(files, pattern) {
94
+ if (files.length > 0) return files.map((file) => `\`${file}\``).join(", ");
95
+ if (pattern) return `files matching ${pattern.toString()}`;
96
+ return `\`${LOCALGHOST_CONFIG_FILE}\``;
97
+ }
98
+ function readDevHosts(options = {}) {
99
+ const resolvedOptions = typeof options === "string" ? { cwd: options } : options;
100
+ const resolvedPath = resolveDevHostsPath(resolvedOptions);
101
+ if (!resolvedPath.exists) {
102
+ const cwd = resolvedOptions.cwd ?? process.cwd();
103
+ throw new Error(
104
+ `Missing Localghost config in ${cwd}. Looked for ${formatSearchedFiles(resolvedPath.searchedFiles, resolvedPath.configPattern)}. Run \`localghost init\` or pass --config/--config-pattern.`
105
+ );
106
+ }
107
+ return parseDevHosts(readFileSync(resolvedPath.path, "utf8"), resolvedPath.fileName);
108
+ }
109
+ function getProjectName(cwd = process.cwd()) {
110
+ try {
111
+ const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
112
+ const name = typeof pkg.name === "string" && pkg.name ? pkg.name : "app";
113
+ return sanitizeProjectName(name.replace(/^@/, ""));
114
+ } catch {
115
+ return "app";
116
+ }
117
+ }
118
+ function sanitizeProjectName(value) {
119
+ const projectName = value.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
120
+ return projectName || "app";
121
+ }
122
+
123
+ // src/caddy.ts
124
+ import { dirname as dirname2, join as join2 } from "path";
125
+ import { execa } from "execa";
126
+
127
+ // src/fs.ts
128
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
129
+ import { dirname } from "path";
130
+ function readTextFile(path) {
131
+ return readFileSync2(path, "utf8");
132
+ }
133
+ function writeTextFile(path, value) {
134
+ mkdirSync(dirname(path), { recursive: true });
135
+ writeFileSync(path, value, "utf8");
136
+ return path;
137
+ }
138
+
139
+ // src/caddy.ts
140
+ function groupByPort(entries) {
141
+ const groups = /* @__PURE__ */ new Map();
142
+ for (const entry of entries) {
143
+ const group = groups.get(entry.port) ?? [];
144
+ group.push(entry);
145
+ groups.set(entry.port, group);
146
+ }
147
+ return groups;
148
+ }
149
+ function getCaddyfilePath(cwd = process.cwd()) {
150
+ return join2(cwd, "ops/local/Caddyfile");
151
+ }
152
+ function renderCaddyfile(entries) {
153
+ const groups = groupByPort(entries);
154
+ const blocks = [...groups.entries()].sort(([leftPort], [rightPort]) => leftPort - rightPort).map(([port, group]) => {
155
+ const hosts = group.map((entry) => entry.host).sort().join(", ");
156
+ return `${hosts} {
157
+ reverse_proxy 127.0.0.1:${port}
158
+ }`;
159
+ });
160
+ return `{
161
+ local_certs
162
+ }
163
+
164
+ ${blocks.join("\n\n")}
165
+ `;
166
+ }
167
+ async function writeCaddyfile(entries, cwd = process.cwd()) {
168
+ const path = getCaddyfilePath(cwd);
169
+ writeTextFile(path, renderCaddyfile(entries));
170
+ return path;
171
+ }
172
+ async function validateCaddyfile(path) {
173
+ await execa("caddy", ["validate", "--config", path], {
174
+ cwd: dirname2(path),
175
+ stdio: "inherit"
176
+ });
177
+ }
178
+ async function runCaddy(path) {
179
+ await execa("caddy", ["run", "--config", path], {
180
+ cwd: dirname2(path),
181
+ stdio: "inherit"
182
+ });
183
+ }
184
+
185
+ // src/doctor.ts
186
+ import { execa as execa2 } from "execa";
187
+ async function checkCaddy() {
188
+ try {
189
+ const result = await execa2("caddy", ["version"], { reject: false });
190
+ const version = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
191
+ return {
192
+ found: result.exitCode === 0,
193
+ ...version ? { version } : {},
194
+ installHint: "brew install caddy"
195
+ };
196
+ } catch {
197
+ return {
198
+ found: false,
199
+ installHint: "brew install caddy"
200
+ };
201
+ }
202
+ }
203
+ async function runDoctor() {
204
+ const caddy = await checkCaddy();
205
+ return {
206
+ ok: caddy.found,
207
+ caddy
208
+ };
209
+ }
210
+
211
+ // src/hosts-file.ts
212
+ import { writeFileSync as writeFileSync2 } from "fs";
213
+ import { tmpdir } from "os";
214
+ import { join as join3 } from "path";
215
+ import { execa as execa3 } from "execa";
216
+ function escapeRegExp(value) {
217
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
218
+ }
219
+ function getManagedBlockPattern(projectName) {
220
+ const sanitizedProjectName = sanitizeProjectName(projectName);
221
+ const start = `# localghost:start ${sanitizedProjectName}`;
222
+ const end = `# localghost:end ${sanitizedProjectName}`;
223
+ return new RegExp(`${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}\\n?`, "m");
224
+ }
225
+ function getSystemHostsPath() {
226
+ return process.platform === "win32" ? "C:\\Windows\\System32\\drivers\\etc\\hosts" : "/etc/hosts";
227
+ }
228
+ function renderHostsBlock(projectName, entries) {
229
+ const sanitizedProjectName = sanitizeProjectName(projectName);
230
+ const hosts = [...new Set(entries.map((entry) => entry.host))].sort();
231
+ return [
232
+ `# localghost:start ${sanitizedProjectName}`,
233
+ ...hosts.map((host) => `127.0.0.1 ${host}`),
234
+ `# localghost:end ${sanitizedProjectName}`,
235
+ ""
236
+ ].join("\n");
237
+ }
238
+ function upsertManagedBlock(existing, projectName, block) {
239
+ const pattern = getManagedBlockPattern(projectName);
240
+ if (pattern.test(existing)) {
241
+ return existing.replace(pattern, block);
242
+ }
243
+ return `${existing.trimEnd()}
244
+
245
+ ${block}`;
246
+ }
247
+ function removeManagedBlock(existing, projectName) {
248
+ const pattern = getManagedBlockPattern(projectName);
249
+ if (!pattern.test(existing)) {
250
+ return existing;
251
+ }
252
+ return existing.replace(pattern, "").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
253
+ }
254
+ async function writeSystemHostsFile(hostsPath, next, projectName) {
255
+ const sanitizedProjectName = sanitizeProjectName(projectName);
256
+ const tempPath = join3(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
257
+ writeFileSync2(tempPath, next, "utf8");
258
+ if (process.platform === "win32") {
259
+ throw new Error(`Windows support: run as administrator and copy ${tempPath} to ${hostsPath}.`);
260
+ }
261
+ await execa3("sudo", ["cp", tempPath, hostsPath], { stdio: "inherit" });
262
+ return tempPath;
263
+ }
264
+ async function updateSystemHosts(projectName, entries) {
265
+ const sanitizedProjectName = sanitizeProjectName(projectName);
266
+ const hostsPath = getSystemHostsPath();
267
+ const existing = readTextFile(hostsPath);
268
+ const block = renderHostsBlock(sanitizedProjectName, entries);
269
+ const next = upsertManagedBlock(existing, sanitizedProjectName, block);
270
+ if (next === existing) {
271
+ return { changed: false, hostsPath };
272
+ }
273
+ const tempPath = await writeSystemHostsFile(hostsPath, next, sanitizedProjectName);
274
+ return { changed: true, hostsPath, tempPath };
275
+ }
276
+ async function removeSystemHosts(projectName) {
277
+ const sanitizedProjectName = sanitizeProjectName(projectName);
278
+ const hostsPath = getSystemHostsPath();
279
+ const existing = readTextFile(hostsPath);
280
+ const next = removeManagedBlock(existing, sanitizedProjectName);
281
+ if (next === existing) {
282
+ return { changed: false, removed: false, hostsPath };
283
+ }
284
+ const tempPath = await writeSystemHostsFile(hostsPath, next, sanitizedProjectName);
285
+ return { changed: true, removed: true, hostsPath, tempPath };
286
+ }
287
+
288
+ // src/init.ts
289
+ import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
290
+ import { join as join4 } from "path";
291
+ function detectPackageManager(cwd = process.cwd()) {
292
+ if (existsSync2(join4(cwd, "pnpm-lock.yaml"))) return "pnpm";
293
+ if (existsSync2(join4(cwd, "yarn.lock"))) return "yarn";
294
+ return "npm";
295
+ }
296
+ function packageRunCommand(packageManager, script) {
297
+ if (packageManager === "yarn") return `yarn ${script}`;
298
+ if (packageManager === "pnpm") return `pnpm ${script}`;
299
+ return `npm run ${script}`;
300
+ }
301
+ function packageAddCommand(packageManager, packageName = "@hamedb89/localghost") {
302
+ if (packageManager === "yarn") return `yarn add -D ${packageName}`;
303
+ if (packageManager === "pnpm") return `pnpm add -D ${packageName}`;
304
+ return `npm install -D ${packageName}`;
305
+ }
306
+ function renderConfig(options) {
307
+ return [
308
+ "# Buh. Friendly names for local services.",
309
+ "# Format: <host> <port>",
310
+ `${options.host} ${options.port}`,
311
+ `www.${options.host} ${options.port}`,
312
+ `${options.apiHost} ${options.apiPort}`,
313
+ ""
314
+ ].join("\n");
315
+ }
316
+ function readPackageJson(path) {
317
+ try {
318
+ return JSON.parse(readFileSync3(path, "utf8"));
319
+ } catch {
320
+ return null;
321
+ }
322
+ }
323
+ function shellQuote(value) {
324
+ if (/^[A-Za-z0-9_./:-]+$/.test(value)) return value;
325
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
326
+ }
327
+ function getConfigFlag(configFile) {
328
+ return configFile === LOCALGHOST_CONFIG_FILE ? "" : ` --config ${shellQuote(configFile)}`;
329
+ }
330
+ function updatePackageScripts(packageJsonPath, configFile) {
331
+ const pkg = readPackageJson(packageJsonPath);
332
+ if (!pkg) return false;
333
+ const scripts = typeof pkg.scripts === "object" && pkg.scripts ? pkg.scripts : {};
334
+ const configFlag = getConfigFlag(configFile);
335
+ const nextScripts = {
336
+ ...scripts,
337
+ "localghost:setup": scripts["localghost:setup"] ?? `localghost setup${configFlag}`,
338
+ "localghost:proxy": scripts["localghost:proxy"] ?? `localghost dev${configFlag}`,
339
+ "localghost:print": scripts["localghost:print"] ?? `localghost print${configFlag}`,
340
+ "localghost:routes": scripts["localghost:routes"] ?? `localghost routes${configFlag}`,
341
+ "localghost:status": scripts["localghost:status"] ?? "localghost status",
342
+ "localghost:teardown": scripts["localghost:teardown"] ?? "localghost teardown",
343
+ "localghost:doctor": scripts["localghost:doctor"] ?? "localghost doctor",
344
+ "localghost:update": scripts["localghost:update"] ?? "localghost update"
345
+ };
346
+ const changed = JSON.stringify(scripts) !== JSON.stringify(nextScripts);
347
+ if (!changed) return false;
348
+ pkg.scripts = nextScripts;
349
+ writeFileSync3(packageJsonPath, `${JSON.stringify(pkg, null, 2)}
350
+ `, "utf8");
351
+ return true;
352
+ }
353
+ function initLocalghost(options = {}) {
354
+ const cwd = options.cwd ?? process.cwd();
355
+ const projectName = sanitizeProjectName(getProjectName(cwd).split("/").pop() ?? "app");
356
+ const host = options.host ?? `${projectName}.localhost`;
357
+ const port = options.port ?? 5173;
358
+ const apiHost = options.apiHost ?? `api.${host}`;
359
+ const apiPort = options.apiPort ?? 8787;
360
+ const packageManager = options.packageManager ?? detectPackageManager(cwd);
361
+ const configFile = options.configFile ?? LOCALGHOST_CONFIG_FILE;
362
+ const configPath = join4(cwd, configFile);
363
+ const configExists = existsSync2(configPath);
364
+ if (configExists && !options.force) {
365
+ return {
366
+ configPath,
367
+ configCreated: false,
368
+ packageJsonChanged: false,
369
+ packageManager,
370
+ nextSteps: [
371
+ packageRunCommand(packageManager, "localghost:doctor"),
372
+ packageRunCommand(packageManager, "localghost:setup"),
373
+ packageRunCommand(packageManager, "localghost:proxy")
374
+ ]
375
+ };
376
+ }
377
+ writeTextFile(configPath, renderConfig({ host, port, apiHost, apiPort }));
378
+ const packageJsonPath = join4(cwd, "package.json");
379
+ const packageJsonChanged = options.writeScripts ? updatePackageScripts(packageJsonPath, configFile) : false;
380
+ return {
381
+ configPath,
382
+ configCreated: true,
383
+ ...existsSync2(packageJsonPath) ? { packageJsonPath } : {},
384
+ packageJsonChanged,
385
+ packageManager,
386
+ nextSteps: [
387
+ packageRunCommand(packageManager, "localghost:doctor"),
388
+ packageRunCommand(packageManager, "localghost:setup"),
389
+ packageRunCommand(packageManager, "localghost:proxy")
390
+ ]
391
+ };
392
+ }
393
+
394
+ // src/routes.ts
395
+ function getDomainRoutes(entries, options = {}) {
396
+ const protocol = options.https === false ? "http" : "https";
397
+ return [...entries].sort((left, right) => left.host.localeCompare(right.host) || left.port - right.port).map((entry) => ({
398
+ host: entry.host,
399
+ port: entry.port,
400
+ url: `${protocol}://${entry.host}/`,
401
+ upstream: `http://${entry.target}`
402
+ }));
403
+ }
404
+ function formatDomainRoutes(entries, options = {}) {
405
+ const routes = getDomainRoutes(entries, options);
406
+ if (routes.length === 0) {
407
+ return "localghost routes\n no routes";
408
+ }
409
+ return [
410
+ "localghost routes",
411
+ ...routes.map((route) => ` ${route.url} -> ${route.upstream}`)
412
+ ].join("\n");
413
+ }
414
+
415
+ // src/state.ts
416
+ import { existsSync as existsSync3 } from "fs";
417
+ import { join as join5 } from "path";
418
+ var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
419
+ function getLocalghostStatePath(cwd = process.cwd()) {
420
+ return join5(cwd, LOCALGHOST_STATE_FILE);
421
+ }
422
+ function readLocalghostState(cwd = process.cwd()) {
423
+ const path = getLocalghostStatePath(cwd);
424
+ if (!existsSync3(path)) return null;
425
+ return JSON.parse(readTextFile(path));
426
+ }
427
+ function writeLocalghostState(cwd, state) {
428
+ const path = getLocalghostStatePath(cwd);
429
+ writeTextFile(path, `${JSON.stringify({ version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), ...state }, null, 2)}
430
+ `);
431
+ return path;
432
+ }
433
+
434
+ // src/update-check.ts
435
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
436
+ import { homedir } from "os";
437
+ import { dirname as dirname3, join as join6 } from "path";
438
+ var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
439
+ var LOCALGHOST_VERSION = "0.1.0";
440
+ var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
441
+ var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
442
+ var UPDATE_CHECK_TIMEOUT_MS = 900;
443
+ function truthyEnv(value) {
444
+ return value === "1" || value === "true" || value === "yes";
445
+ }
446
+ function isUpdateCheckDisabled(env = process.env) {
447
+ return truthyEnv(env.LOCALGHOST_NO_UPDATE_CHECK);
448
+ }
449
+ function getUpdateCheckCachePath(env = process.env) {
450
+ if (env.LOCALGHOST_UPDATE_CHECK_CACHE) return env.LOCALGHOST_UPDATE_CHECK_CACHE;
451
+ const cacheRoot = env.XDG_CACHE_HOME || join6(homedir(), ".cache");
452
+ return join6(cacheRoot, "localghost", "update-check.json");
453
+ }
454
+ function readCache(path = getUpdateCheckCachePath()) {
455
+ if (!existsSync4(path)) return null;
456
+ try {
457
+ return JSON.parse(readFileSync4(path, "utf8"));
458
+ } catch {
459
+ return null;
460
+ }
461
+ }
462
+ function writeCache(cache, path = getUpdateCheckCachePath()) {
463
+ try {
464
+ mkdirSync2(dirname3(path), { recursive: true });
465
+ writeFileSync4(path, `${JSON.stringify(cache, null, 2)}
466
+ `, "utf8");
467
+ } catch {
468
+ }
469
+ }
470
+ function ageMs(date, now = Date.now()) {
471
+ if (!date) return Number.POSITIVE_INFINITY;
472
+ const time = Date.parse(date);
473
+ return Number.isFinite(time) ? now - time : Number.POSITIVE_INFINITY;
474
+ }
475
+ function isCacheFresh(cache, ttlMs, now = Date.now()) {
476
+ return Boolean(cache?.latestVersion && ageMs(cache.checkedAt, now) >= 0 && ageMs(cache.checkedAt, now) < ttlMs);
477
+ }
478
+ function parseVersion(version) {
479
+ const match = version.trim().replace(/^v/, "").match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
480
+ if (!match) return null;
481
+ return {
482
+ major: Number(match[1]),
483
+ minor: Number(match[2]),
484
+ patch: Number(match[3]),
485
+ ...match[4] ? { prerelease: match[4] } : {}
486
+ };
487
+ }
488
+ function compareVersions(a, b) {
489
+ const left = parseVersion(a);
490
+ const right = parseVersion(b);
491
+ if (!left || !right) return a.localeCompare(b);
492
+ for (const key of ["major", "minor", "patch"]) {
493
+ if (left[key] !== right[key]) return left[key] > right[key] ? 1 : -1;
494
+ }
495
+ if (left.prerelease === right.prerelease) return 0;
496
+ if (!left.prerelease) return 1;
497
+ if (!right.prerelease) return -1;
498
+ return left.prerelease.localeCompare(right.prerelease);
499
+ }
500
+ function isNewerVersion(candidate, current = LOCALGHOST_VERSION) {
501
+ return Boolean(candidate && compareVersions(candidate, current) > 0);
502
+ }
503
+ async function fetchLatestVersion(packageName, timeoutMs) {
504
+ const encodedName = packageName.startsWith("@") ? `@${packageName.slice(1).replace("/", "%2f")}` : packageName;
505
+ const response = await fetch(`https://registry.npmjs.org/${encodedName}`, {
506
+ signal: AbortSignal.timeout(timeoutMs),
507
+ headers: {
508
+ accept: "application/vnd.npm.install-v1+json"
509
+ }
510
+ });
511
+ if (!response.ok) throw new Error(`npm registry returned ${response.status}`);
512
+ const data = await response.json();
513
+ const latest = data["dist-tags"]?.latest;
514
+ if (typeof latest !== "string" || latest.length === 0) throw new Error("npm registry response did not include latest dist-tag");
515
+ return latest;
516
+ }
517
+ async function checkForUpdate(options = {}) {
518
+ const env = options.env ?? process.env;
519
+ const packageName = options.packageName ?? LOCALGHOST_PACKAGE_NAME;
520
+ const currentVersion = options.currentVersion ?? LOCALGHOST_VERSION;
521
+ const cachePath = options.cachePath ?? getUpdateCheckCachePath(env);
522
+ if (!options.force && isUpdateCheckDisabled(env)) {
523
+ return {
524
+ currentVersion,
525
+ packageName,
526
+ updateAvailable: false,
527
+ source: "disabled"
528
+ };
529
+ }
530
+ const cache = readCache(cachePath);
531
+ if (!options.force && isCacheFresh(cache, UPDATE_CHECK_CACHE_TTL_MS)) {
532
+ const latestVersion = cache?.latestVersion;
533
+ return {
534
+ currentVersion,
535
+ packageName,
536
+ ...latestVersion ? { latestVersion } : {},
537
+ updateAvailable: isNewerVersion(latestVersion, currentVersion),
538
+ source: "cache"
539
+ };
540
+ }
541
+ try {
542
+ const latestVersion = await fetchLatestVersion(packageName, options.timeoutMs ?? UPDATE_CHECK_TIMEOUT_MS);
543
+ writeCache({ checkedAt: (/* @__PURE__ */ new Date()).toISOString(), latestVersion }, cachePath);
544
+ return {
545
+ currentVersion,
546
+ packageName,
547
+ latestVersion,
548
+ updateAvailable: isNewerVersion(latestVersion, currentVersion),
549
+ source: "registry"
550
+ };
551
+ } catch (error) {
552
+ const latestVersion = cache?.latestVersion;
553
+ return {
554
+ currentVersion,
555
+ packageName,
556
+ ...latestVersion ? { latestVersion } : {},
557
+ updateAvailable: isNewerVersion(latestVersion, currentVersion),
558
+ source: "error",
559
+ error: error instanceof Error ? error.message : String(error)
560
+ };
561
+ }
562
+ }
563
+ function formatUpdateMessage(result) {
564
+ if (!result.updateAvailable || !result.latestVersion) return null;
565
+ return [
566
+ `localghost ${result.latestVersion} is available. Current: ${result.currentVersion}`,
567
+ `Update with: npm i -g ${result.packageName}@latest`
568
+ ].join("\n");
569
+ }
570
+ function shouldNotifyAboutUpdate(result, cachePath = getUpdateCheckCachePath(), now = Date.now()) {
571
+ if (!result.updateAvailable || !result.latestVersion) return false;
572
+ const cache = readCache(cachePath);
573
+ if (cache?.notifiedVersion !== result.latestVersion) return true;
574
+ return ageMs(cache.notifiedAt, now) >= UPDATE_CHECK_NOTIFY_TTL_MS;
575
+ }
576
+ function markUpdateNotified(result, cachePath = getUpdateCheckCachePath()) {
577
+ if (!result.latestVersion) return;
578
+ const cache = readCache(cachePath) ?? { checkedAt: (/* @__PURE__ */ new Date()).toISOString() };
579
+ writeCache(
580
+ {
581
+ ...cache,
582
+ latestVersion: result.latestVersion,
583
+ notifiedVersion: result.latestVersion,
584
+ notifiedAt: (/* @__PURE__ */ new Date()).toISOString()
585
+ },
586
+ cachePath
587
+ );
588
+ }
589
+ async function maybeNotifyAboutUpdate(options = {}) {
590
+ if (options.disabled) return;
591
+ const cachePath = getUpdateCheckCachePath();
592
+ const result = await checkForUpdate({ cachePath });
593
+ if (!shouldNotifyAboutUpdate(result, cachePath)) return;
594
+ const message = formatUpdateMessage(result);
595
+ if (!message) return;
596
+ console.warn(`
597
+ ${message}`);
598
+ markUpdateNotified(result, cachePath);
599
+ }
600
+ export {
601
+ LOCALGHOST_CONFIG_FILE,
602
+ LOCALGHOST_PACKAGE_NAME,
603
+ LOCALGHOST_STATE_FILE,
604
+ LOCALGHOST_VERSION,
605
+ UPDATE_CHECK_CACHE_TTL_MS,
606
+ UPDATE_CHECK_NOTIFY_TTL_MS,
607
+ UPDATE_CHECK_TIMEOUT_MS,
608
+ checkCaddy,
609
+ checkForUpdate,
610
+ compareVersions,
611
+ detectPackageManager,
612
+ findLocalMdnsHosts,
613
+ formatDomainRoutes,
614
+ formatUpdateMessage,
615
+ getCaddyfilePath,
616
+ getConfigFileCandidates,
617
+ getDevHostsPath,
618
+ getDomainRoutes,
619
+ getLocalghostStatePath,
620
+ getProjectName,
621
+ getSystemHostsPath,
622
+ getUpdateCheckCachePath,
623
+ initLocalghost,
624
+ isNewerVersion,
625
+ isUpdateCheckDisabled,
626
+ markUpdateNotified,
627
+ maybeNotifyAboutUpdate,
628
+ packageAddCommand,
629
+ packageRunCommand,
630
+ parseDevHosts,
631
+ readDevHosts,
632
+ readLocalghostState,
633
+ removeManagedBlock,
634
+ removeSystemHosts,
635
+ renderCaddyfile,
636
+ renderHostsBlock,
637
+ resolveDevHostsPath,
638
+ runCaddy,
639
+ runDoctor,
640
+ sanitizeProjectName,
641
+ shouldNotifyAboutUpdate,
642
+ updateSystemHosts,
643
+ upsertManagedBlock,
644
+ validateCaddyfile,
645
+ writeCaddyfile,
646
+ writeLocalghostState
647
+ };
648
+ //# sourceMappingURL=index.js.map