@hamedb89/localghost 0.1.0 → 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/README.md +100 -13
- package/dist/cli.js +645 -83
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +102 -6
- package/dist/index.js +294 -50
- package/dist/index.js.map +1 -1
- package/dist/vite.d.ts +3 -0
- package/dist/vite.js +460 -16
- package/dist/vite.js.map +1 -1
- package/docs/flows.md +27 -5
- package/docs/github.md +46 -8
- package/docs/localghost.1.md +45 -9
- package/package.json +7 -4
package/dist/vite.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/config.ts","../src/parse.ts","../src/vite.ts"],"sourcesContent":["import { existsSync, readFileSync, readdirSync } from \"node:fs\";\nimport { basename, join, resolve } from \"node:path\";\nimport { parseDevHosts } from \"./parse.js\";\n\nexport const LOCALGHOST_CONFIG_FILE = \".localghost\";\n\nexport type ConfigPattern = string | RegExp;\n\nexport type ReadDevHostsOptions = {\n cwd?: string;\n fileName?: string;\n configFiles?: string[];\n configPattern?: ConfigPattern;\n};\n\nexport type ResolvedDevHostsPath = {\n path: string;\n fileName: string;\n exists: boolean;\n searchedFiles: string[];\n configPattern?: ConfigPattern;\n};\n\nfunction unique(values: string[]) {\n return [...new Set(values.filter(Boolean))];\n}\n\nfunction toRegExp(pattern: ConfigPattern) {\n return typeof pattern === \"string\" ? new RegExp(pattern) : pattern;\n}\n\nfunction findPatternMatches(cwd: string, pattern: ConfigPattern) {\n const matcher = toRegExp(pattern);\n\n return readdirSync(cwd, { withFileTypes: true })\n .filter((entry) => entry.isFile())\n .map((entry) => entry.name)\n .filter((name) => {\n matcher.lastIndex = 0;\n return matcher.test(name);\n })\n .sort();\n}\n\nexport function getConfigFileCandidates(options: ReadDevHostsOptions = {}) {\n const cwd = options.cwd ?? process.cwd();\n const exactFiles = unique([\n ...(options.fileName ? [options.fileName] : []),\n ...(options.configFiles ?? [])\n ]);\n const patternFiles = options.configPattern ? findPatternMatches(cwd, options.configPattern) : [];\n const candidates = unique([...exactFiles, ...patternFiles]);\n\n if (candidates.length > 0) return candidates;\n if (exactFiles.length > 0 || options.configPattern) return [];\n return [LOCALGHOST_CONFIG_FILE];\n}\n\nexport function resolveDevHostsPath(options: ReadDevHostsOptions = {}): ResolvedDevHostsPath {\n const cwd = options.cwd ?? process.cwd();\n const searchedFiles = getConfigFileCandidates(options);\n\n for (const fileName of searchedFiles) {\n const path = resolve(cwd, fileName);\n if (existsSync(path)) {\n return {\n path,\n fileName: basename(fileName),\n exists: true,\n searchedFiles,\n ...(options.configPattern ? { configPattern: options.configPattern } : {})\n };\n }\n }\n\n const fileName = searchedFiles[0] ?? LOCALGHOST_CONFIG_FILE;\n\n return {\n path: resolve(cwd, fileName),\n fileName: basename(fileName),\n exists: false,\n searchedFiles,\n ...(options.configPattern ? { configPattern: options.configPattern } : {})\n };\n}\n\nexport function getDevHostsPath(options: ReadDevHostsOptions = {}) {\n return resolveDevHostsPath(options).path;\n}\n\nfunction formatSearchedFiles(files: string[], pattern?: ConfigPattern) {\n if (files.length > 0) return files.map((file) => `\\`${file}\\``).join(\", \");\n if (pattern) return `files matching ${pattern.toString()}`;\n return `\\`${LOCALGHOST_CONFIG_FILE}\\``;\n}\n\nexport function readDevHosts(options: ReadDevHostsOptions | string = {}) {\n const resolvedOptions = typeof options === \"string\" ? { cwd: options } : options;\n const resolvedPath = resolveDevHostsPath(resolvedOptions);\n\n if (!resolvedPath.exists) {\n const cwd = resolvedOptions.cwd ?? process.cwd();\n throw new Error(\n `Missing Localghost config in ${cwd}. Looked for ${formatSearchedFiles(resolvedPath.searchedFiles, resolvedPath.configPattern)}. Run \\`localghost init\\` or pass --config/--config-pattern.`\n );\n }\n\n return parseDevHosts(readFileSync(resolvedPath.path, \"utf8\"), resolvedPath.fileName);\n}\n\nexport function getProjectName(cwd = process.cwd()) {\n try {\n const pkg = JSON.parse(readFileSync(join(cwd, \"package.json\"), \"utf8\")) as { name?: unknown };\n const name = typeof pkg.name === \"string\" && pkg.name ? pkg.name : \"app\";\n return sanitizeProjectName(name.replace(/^@/, \"\"));\n } catch {\n return \"app\";\n }\n}\n\nexport function sanitizeProjectName(value: string) {\n const projectName = value.replace(/[^\\w.-]+/g, \"-\").replace(/^-+|-+$/g, \"\");\n return projectName || \"app\";\n}\n","export type DevHostEntry = {\n host: string;\n port: number;\n target: string;\n};\n\nconst HOST_PATTERN = /^(?=.{1,253}$)(?!-)[a-z0-9-]+(\\.[a-z0-9-]+)*\\.?$/i;\n\nexport function parseDevHosts(input: string, fileName = \".localghost\"): DevHostEntry[] {\n const entries: DevHostEntry[] = [];\n\n input.split(/\\r?\\n/).forEach((rawLine, index) => {\n const line = rawLine.replace(/#.*/, \"\").trim();\n\n if (!line) {\n return;\n }\n\n const parts = line.split(/\\s+/);\n const host = parts[0];\n const portRaw = parts[1];\n\n if (!host || !portRaw || parts.length > 2) {\n throw new Error(`Invalid ${fileName} line ${index + 1}: \"${rawLine}\"`);\n }\n\n if (!HOST_PATTERN.test(host)) {\n throw new Error(`Invalid host on line ${index + 1}: \"${host}\"`);\n }\n\n const port = Number(portRaw);\n\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw new Error(`Invalid port on line ${index + 1}: \"${portRaw}\"`);\n }\n\n entries.push({\n host: host.toLowerCase().replace(/\\.$/, \"\"),\n port,\n target: `127.0.0.1:${port}`\n });\n });\n\n return entries;\n}\n\nexport function findLocalMdnsHosts(entries: DevHostEntry[]): string[] {\n return [...new Set(entries.map((entry) => entry.host).filter((host) => host.endsWith(\".local\")))];\n}\n","import type { HmrOptions, Plugin, UserConfig, ViteDevServer, WsOptions } from \"vite\";\nimport { readDevHosts, type ConfigPattern, type ReadDevHostsOptions } from \"./config.js\";\nimport type { DevHostEntry } from \"./parse.js\";\n\nexport type LocalGhostPluginOptions = {\n cwd?: string;\n fileName?: string;\n configFiles?: string[];\n configPattern?: ConfigPattern;\n port?: number;\n https?: boolean;\n primaryHost?: string;\n log?: boolean;\n};\n\ntype ServerOptions = NonNullable<UserConfig[\"server\"]>;\n\nfunction mergeAllowedHosts(current: ServerOptions[\"allowedHosts\"], hosts: string[]) {\n if (Array.isArray(current)) {\n return [...new Set([...current, ...hosts])];\n }\n\n return hosts;\n}\n\nfunction getDisplayEntries(entries: DevHostEntry[], vitePort: number | undefined) {\n if (!vitePort) {\n return entries;\n }\n\n const matchingEntries = entries.filter((entry) => entry.port === vitePort);\n return matchingEntries.length > 0 ? matchingEntries : entries;\n}\n\nfunction printLocalHosts(server: ViteDevServer, entries: DevHostEntry[], vitePort: number | undefined, https: boolean) {\n const displayEntries = getDisplayEntries(entries, vitePort);\n const protocol = https ? \"https\" : \"http\";\n const urls = displayEntries.map((entry) => `${protocol}://${entry.host}/`);\n const primaryUrl = urls[0];\n\n if (!primaryUrl) {\n return;\n }\n\n const lines = [\n \"\",\n \" localghost\",\n ` open: ${primaryUrl}`,\n ...urls.slice(1).map((url) => ` also: ${url}`),\n vitePort ? ` target: http://127.0.0.1:${vitePort}/` : undefined,\n https ? \" proxy: Caddy local HTTPS\" : undefined\n ].filter((line): line is string => Boolean(line));\n\n server.config.logger.info(lines.join(\"\\n\"), {\n clear: false,\n timestamp: false\n });\n}\n\nfunction readOptionsFromPlugin(options: LocalGhostPluginOptions): ReadDevHostsOptions {\n return {\n cwd: options.cwd ?? process.cwd(),\n ...(options.fileName ? { fileName: options.fileName } : {}),\n ...(options.configFiles ? { configFiles: options.configFiles } : {}),\n ...(options.configPattern ? { configPattern: options.configPattern } : {})\n };\n}\n\nexport function localGhostPlugin(options: LocalGhostPluginOptions = {}): Plugin {\n let resolvedEntries: DevHostEntry[] = [];\n let resolvedVitePort: number | undefined;\n\n return {\n name: \"localghost:vite\",\n enforce: \"pre\",\n\n config(userConfig): UserConfig {\n const entries = readDevHosts(readOptionsFromPlugin(options));\n const hosts = [...new Set(entries.map((entry) => entry.host))];\n const existingServer = userConfig.server ?? {};\n const vitePort =\n options.port ??\n existingServer.port ??\n entries.find((entry) => !entry.host.startsWith(\"api.\"))?.port ??\n entries[0]?.port;\n const primaryHost =\n options.primaryHost ??\n entries.find((entry) => entry.port === vitePort)?.host ??\n hosts[0];\n\n resolvedEntries = entries;\n resolvedVitePort = vitePort;\n\n const server: ServerOptions = {\n ...existingServer,\n allowedHosts: mergeAllowedHosts(existingServer.allowedHosts, hosts),\n strictPort: existingServer.strictPort ?? true\n };\n\n if (vitePort) {\n server.port = vitePort;\n }\n\n if (options.https && primaryHost) {\n const existingWs = typeof server.ws === \"object\" && server.ws ? server.ws : {};\n const existingHmr = typeof existingServer.hmr === \"object\" && existingServer.hmr ? existingServer.hmr : {};\n\n server.ws = {\n ...existingWs,\n protocol: \"wss\",\n host: primaryHost,\n clientPort: 443\n } satisfies WsOptions;\n\n server.hmr = {\n ...existingHmr,\n protocol: \"wss\",\n host: primaryHost,\n clientPort: 443\n } satisfies HmrOptions;\n }\n\n return { server };\n },\n\n configureServer(server) {\n if (options.log === false) {\n return;\n }\n\n server.httpServer?.once(\"listening\", () => {\n printLocalHosts(server, resolvedEntries, resolvedVitePort, Boolean(options.https));\n });\n }\n };\n}\n\nexport const localHostsPlugin = localGhostPlugin;\nexport type LocalHostsPluginOptions = LocalGhostPluginOptions;\n"],"mappings":";AAAA,SAAS,YAAY,cAAc,mBAAmB;AACtD,SAAS,UAAU,MAAM,eAAe;;;ACKxC,IAAM,eAAe;AAEd,SAAS,cAAc,OAAe,WAAW,eAA+B;AACrF,QAAM,UAA0B,CAAC;AAEjC,QAAM,MAAM,OAAO,EAAE,QAAQ,CAAC,SAAS,UAAU;AAC/C,UAAM,OAAO,QAAQ,QAAQ,OAAO,EAAE,EAAE,KAAK;AAE7C,QAAI,CAAC,MAAM;AACT;AAAA,IACF;AAEA,UAAM,QAAQ,KAAK,MAAM,KAAK;AAC9B,UAAM,OAAO,MAAM,CAAC;AACpB,UAAM,UAAU,MAAM,CAAC;AAEvB,QAAI,CAAC,QAAQ,CAAC,WAAW,MAAM,SAAS,GAAG;AACzC,YAAM,IAAI,MAAM,WAAW,QAAQ,SAAS,QAAQ,CAAC,MAAM,OAAO,GAAG;AAAA,IACvE;AAEA,QAAI,CAAC,aAAa,KAAK,IAAI,GAAG;AAC5B,YAAM,IAAI,MAAM,wBAAwB,QAAQ,CAAC,MAAM,IAAI,GAAG;AAAA,IAChE;AAEA,UAAM,OAAO,OAAO,OAAO;AAE3B,QAAI,CAAC,OAAO,UAAU,IAAI,KAAK,OAAO,KAAK,OAAO,OAAO;AACvD,YAAM,IAAI,MAAM,wBAAwB,QAAQ,CAAC,MAAM,OAAO,GAAG;AAAA,IACnE;AAEA,YAAQ,KAAK;AAAA,MACX,MAAM,KAAK,YAAY,EAAE,QAAQ,OAAO,EAAE;AAAA,MAC1C;AAAA,MACA,QAAQ,aAAa,IAAI;AAAA,IAC3B,CAAC;AAAA,EACH,CAAC;AAED,SAAO;AACT;;;ADxCO,IAAM,yBAAyB;AAmBtC,SAAS,OAAO,QAAkB;AAChC,SAAO,CAAC,GAAG,IAAI,IAAI,OAAO,OAAO,OAAO,CAAC,CAAC;AAC5C;AAEA,SAAS,SAAS,SAAwB;AACxC,SAAO,OAAO,YAAY,WAAW,IAAI,OAAO,OAAO,IAAI;AAC7D;AAEA,SAAS,mBAAmB,KAAa,SAAwB;AAC/D,QAAM,UAAU,SAAS,OAAO;AAEhC,SAAO,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC,EAC5C,OAAO,CAAC,UAAU,MAAM,OAAO,CAAC,EAChC,IAAI,CAAC,UAAU,MAAM,IAAI,EACzB,OAAO,CAAC,SAAS;AAChB,YAAQ,YAAY;AACpB,WAAO,QAAQ,KAAK,IAAI;AAAA,EAC1B,CAAC,EACA,KAAK;AACV;AAEO,SAAS,wBAAwB,UAA+B,CAAC,GAAG;AACzE,QAAM,MAAM,QAAQ,OAAO,QAAQ,IAAI;AACvC,QAAM,aAAa,OAAO;AAAA,IACxB,GAAI,QAAQ,WAAW,CAAC,QAAQ,QAAQ,IAAI,CAAC;AAAA,IAC7C,GAAI,QAAQ,eAAe,CAAC;AAAA,EAC9B,CAAC;AACD,QAAM,eAAe,QAAQ,gBAAgB,mBAAmB,KAAK,QAAQ,aAAa,IAAI,CAAC;AAC/F,QAAM,aAAa,OAAO,CAAC,GAAG,YAAY,GAAG,YAAY,CAAC;AAE1D,MAAI,WAAW,SAAS,EAAG,QAAO;AAClC,MAAI,WAAW,SAAS,KAAK,QAAQ,cAAe,QAAO,CAAC;AAC5D,SAAO,CAAC,sBAAsB;AAChC;AAEO,SAAS,oBAAoB,UAA+B,CAAC,GAAyB;AAC3F,QAAM,MAAM,QAAQ,OAAO,QAAQ,IAAI;AACvC,QAAM,gBAAgB,wBAAwB,OAAO;AAErD,aAAWA,aAAY,eAAe;AACpC,UAAM,OAAO,QAAQ,KAAKA,SAAQ;AAClC,QAAI,WAAW,IAAI,GAAG;AACpB,aAAO;AAAA,QACL;AAAA,QACA,UAAU,SAASA,SAAQ;AAAA,QAC3B,QAAQ;AAAA,QACR;AAAA,QACA,GAAI,QAAQ,gBAAgB,EAAE,eAAe,QAAQ,cAAc,IAAI,CAAC;AAAA,MAC1E;AAAA,IACF;AAAA,EACF;AAEA,QAAM,WAAW,cAAc,CAAC,KAAK;AAErC,SAAO;AAAA,IACL,MAAM,QAAQ,KAAK,QAAQ;AAAA,IAC3B,UAAU,SAAS,QAAQ;AAAA,IAC3B,QAAQ;AAAA,IACR;AAAA,IACA,GAAI,QAAQ,gBAAgB,EAAE,eAAe,QAAQ,cAAc,IAAI,CAAC;AAAA,EAC1E;AACF;AAMA,SAAS,oBAAoB,OAAiB,SAAyB;AACrE,MAAI,MAAM,SAAS,EAAG,QAAO,MAAM,IAAI,CAAC,SAAS,KAAK,IAAI,IAAI,EAAE,KAAK,IAAI;AACzE,MAAI,QAAS,QAAO,kBAAkB,QAAQ,SAAS,CAAC;AACxD,SAAO,KAAK,sBAAsB;AACpC;AAEO,SAAS,aAAa,UAAwC,CAAC,GAAG;AACvE,QAAM,kBAAkB,OAAO,YAAY,WAAW,EAAE,KAAK,QAAQ,IAAI;AACzE,QAAM,eAAe,oBAAoB,eAAe;AAExD,MAAI,CAAC,aAAa,QAAQ;AACxB,UAAM,MAAM,gBAAgB,OAAO,QAAQ,IAAI;AAC/C,UAAM,IAAI;AAAA,MACR,gCAAgC,GAAG,gBAAgB,oBAAoB,aAAa,eAAe,aAAa,aAAa,CAAC;AAAA,IAChI;AAAA,EACF;AAEA,SAAO,cAAc,aAAa,aAAa,MAAM,MAAM,GAAG,aAAa,QAAQ;AACrF;;;AE3FA,SAAS,kBAAkB,SAAwC,OAAiB;AAClF,MAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,WAAO,CAAC,GAAG,oBAAI,IAAI,CAAC,GAAG,SAAS,GAAG,KAAK,CAAC,CAAC;AAAA,EAC5C;AAEA,SAAO;AACT;AAEA,SAAS,kBAAkB,SAAyB,UAA8B;AAChF,MAAI,CAAC,UAAU;AACb,WAAO;AAAA,EACT;AAEA,QAAM,kBAAkB,QAAQ,OAAO,CAAC,UAAU,MAAM,SAAS,QAAQ;AACzE,SAAO,gBAAgB,SAAS,IAAI,kBAAkB;AACxD;AAEA,SAAS,gBAAgB,QAAuB,SAAyB,UAA8B,OAAgB;AACrH,QAAM,iBAAiB,kBAAkB,SAAS,QAAQ;AAC1D,QAAM,WAAW,QAAQ,UAAU;AACnC,QAAM,OAAO,eAAe,IAAI,CAAC,UAAU,GAAG,QAAQ,MAAM,MAAM,IAAI,GAAG;AACzE,QAAM,aAAa,KAAK,CAAC;AAEzB,MAAI,CAAC,YAAY;AACf;AAAA,EACF;AAEA,QAAM,QAAQ;AAAA,IACZ;AAAA,IACA;AAAA,IACA,aAAa,UAAU;AAAA,IACvB,GAAG,KAAK,MAAM,CAAC,EAAE,IAAI,CAAC,QAAQ,aAAa,GAAG,EAAE;AAAA,IAChD,WAAW,8BAA8B,QAAQ,MAAM;AAAA,IACvD,QAAQ,gCAAgC;AAAA,EAC1C,EAAE,OAAO,CAAC,SAAyB,QAAQ,IAAI,CAAC;AAEhD,SAAO,OAAO,OAAO,KAAK,MAAM,KAAK,IAAI,GAAG;AAAA,IAC1C,OAAO;AAAA,IACP,WAAW;AAAA,EACb,CAAC;AACH;AAEA,SAAS,sBAAsB,SAAuD;AACpF,SAAO;AAAA,IACL,KAAK,QAAQ,OAAO,QAAQ,IAAI;AAAA,IAChC,GAAI,QAAQ,WAAW,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;AAAA,IACzD,GAAI,QAAQ,cAAc,EAAE,aAAa,QAAQ,YAAY,IAAI,CAAC;AAAA,IAClE,GAAI,QAAQ,gBAAgB,EAAE,eAAe,QAAQ,cAAc,IAAI,CAAC;AAAA,EAC1E;AACF;AAEO,SAAS,iBAAiB,UAAmC,CAAC,GAAW;AAC9E,MAAI,kBAAkC,CAAC;AACvC,MAAI;AAEJ,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IAET,OAAO,YAAwB;AAC7B,YAAM,UAAU,aAAa,sBAAsB,OAAO,CAAC;AAC3D,YAAM,QAAQ,CAAC,GAAG,IAAI,IAAI,QAAQ,IAAI,CAAC,UAAU,MAAM,IAAI,CAAC,CAAC;AAC7D,YAAM,iBAAiB,WAAW,UAAU,CAAC;AAC7C,YAAM,WACJ,QAAQ,QACR,eAAe,QACf,QAAQ,KAAK,CAAC,UAAU,CAAC,MAAM,KAAK,WAAW,MAAM,CAAC,GAAG,QACzD,QAAQ,CAAC,GAAG;AACd,YAAM,cACJ,QAAQ,eACR,QAAQ,KAAK,CAAC,UAAU,MAAM,SAAS,QAAQ,GAAG,QAClD,MAAM,CAAC;AAET,wBAAkB;AAClB,yBAAmB;AAEnB,YAAM,SAAwB;AAAA,QAC5B,GAAG;AAAA,QACH,cAAc,kBAAkB,eAAe,cAAc,KAAK;AAAA,QAClE,YAAY,eAAe,cAAc;AAAA,MAC3C;AAEA,UAAI,UAAU;AACZ,eAAO,OAAO;AAAA,MAChB;AAEA,UAAI,QAAQ,SAAS,aAAa;AAChC,cAAM,aAAa,OAAO,OAAO,OAAO,YAAY,OAAO,KAAK,OAAO,KAAK,CAAC;AAC7E,cAAM,cAAc,OAAO,eAAe,QAAQ,YAAY,eAAe,MAAM,eAAe,MAAM,CAAC;AAEzG,eAAO,KAAK;AAAA,UACV,GAAG;AAAA,UACH,UAAU;AAAA,UACV,MAAM;AAAA,UACN,YAAY;AAAA,QACd;AAEA,eAAO,MAAM;AAAA,UACX,GAAG;AAAA,UACH,UAAU;AAAA,UACV,MAAM;AAAA,UACN,YAAY;AAAA,QACd;AAAA,MACF;AAEA,aAAO,EAAE,OAAO;AAAA,IAClB;AAAA,IAEA,gBAAgB,QAAQ;AACtB,UAAI,QAAQ,QAAQ,OAAO;AACzB;AAAA,MACF;AAEA,aAAO,YAAY,KAAK,aAAa,MAAM;AACzC,wBAAgB,QAAQ,iBAAiB,kBAAkB,QAAQ,QAAQ,KAAK,CAAC;AAAA,MACnF,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAEO,IAAM,mBAAmB;","names":["fileName"]}
|
|
1
|
+
{"version":3,"sources":["../src/vite.ts","../src/config.ts","../src/parse.ts","../src/port.ts","../src/context.ts","../src/doctor.ts","../src/env.ts","../src/fs.ts","../src/hosts-file.ts","../src/prompt.ts","../src/state.ts","../src/caddy.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { normalize, resolve } from \"node:path\";\nimport type { ConfigEnv, HmrOptions, Plugin, UserConfig, ViteDevServer, WsOptions } from \"vite\";\nimport {\n getConfigFileCandidates,\n getProjectName,\n resolveDevHostsPath,\n sanitizeProjectName,\n type ConfigPattern,\n type ReadDevHostsOptions\n} from \"./config.js\";\nimport { resolveLocalghostContext, type LocalghostContext } from \"./context.js\";\nimport { checkCaddy } from \"./doctor.js\";\nimport { isProductionLike } from \"./env.js\";\nimport { writeTextFile } from \"./fs.js\";\nimport { getSystemHostsPath, renderHostsBlock, updateSystemHosts } from \"./hosts-file.js\";\nimport { ask, canPrompt, confirm } from \"./prompt.js\";\nimport { getLocalghostStatePath, readLocalghostState, writeLocalghostState } from \"./state.js\";\nimport { getCaddyfilePath, renderCaddyfile, validateCaddyfile, writeCaddyfile } from \"./caddy.js\";\nimport type { DevHostEntry } from \"./parse.js\";\n\nexport type LocalGhostPluginOptions = {\n cwd?: string;\n fileName?: string;\n configFiles?: string[];\n configPattern?: ConfigPattern;\n port?: number;\n https?: boolean;\n bindHost?: string | boolean;\n dynamicPort?: boolean;\n primaryHost?: string;\n log?: boolean;\n setup?: boolean | \"prompt\";\n};\n\ntype ServerOptions = NonNullable<UserConfig[\"server\"]>;\n\nfunction mergeAllowedHosts(current: ServerOptions[\"allowedHosts\"], hosts: string[]) {\n if (Array.isArray(current)) {\n return [...new Set([...current, ...hosts])];\n }\n\n return hosts;\n}\n\nfunction getDisplayEntries(entries: DevHostEntry[], vitePort: number | undefined) {\n if (!vitePort) {\n return entries;\n }\n\n const matchingEntries = entries.filter((entry) => entry.port === vitePort);\n return matchingEntries.length > 0 ? matchingEntries : entries;\n}\n\nfunction printLocalHosts(server: ViteDevServer, entries: DevHostEntry[], vitePort: number | undefined, https: boolean) {\n const displayEntries = getDisplayEntries(entries, vitePort);\n const protocol = https ? \"https\" : \"http\";\n const urls = displayEntries.map((entry) => `${protocol}://${entry.host}/`);\n const primaryUrl = urls[0];\n\n if (!primaryUrl) {\n return;\n }\n\n const lines = [\n \"\",\n \" localghost\",\n ` local: ${primaryUrl}`,\n ...urls.slice(1).map((url) => ` also: ${url}`),\n vitePort ? ` target: http://127.0.0.1:${vitePort}/` : undefined,\n https ? \" proxy: Caddy local HTTPS\" : undefined\n ].filter((line): line is string => Boolean(line));\n\n server.config.logger.info(lines.join(\"\\n\"), {\n clear: false,\n timestamp: false\n });\n}\n\nfunction readOptionsFromPlugin(options: LocalGhostPluginOptions): ReadDevHostsOptions {\n return {\n cwd: options.cwd ?? process.cwd(),\n ...(options.fileName ? { fileName: options.fileName } : {}),\n ...(options.configFiles ? { configFiles: options.configFiles } : {}),\n ...(options.configPattern ? { configPattern: options.configPattern } : {})\n };\n}\n\nfunction getConfigWatchFiles(options: LocalGhostPluginOptions) {\n const readOptions = readOptionsFromPlugin(options);\n const cwd = readOptions.cwd ?? process.cwd();\n const resolvedPath = resolveDevHostsPath(readOptions);\n const candidatePaths = getConfigFileCandidates(readOptions).map((fileName) => resolve(cwd, fileName));\n\n return [...new Set([...candidatePaths, resolvedPath.path])];\n}\n\nfunction normalizeWatchPath(filePath: string) {\n return normalize(resolve(filePath));\n}\n\nfunction renderConfig(hosts: string[], port: number) {\n return [\n \"# Buh. Friendly names for local services.\",\n \"# Format: <host> <port>\",\n ...hosts.map((host) => `${host} ${port}`),\n \"\"\n ].join(\"\\n\");\n}\n\nfunction defaultHost(cwd: string) {\n const projectName = sanitizeProjectName(getProjectName(cwd).split(\"/\").pop() ?? \"app\");\n return `${projectName}.localhost`;\n}\n\nasync function promptForHosts(cwd: string, port: number) {\n const primaryHost = await ask(\"Primary local domain\", defaultHost(cwd));\n const hosts = [primaryHost.toLowerCase()];\n\n while (await confirm(\"Add another local domain?\", false)) {\n const host = await ask(\"Domain\");\n if (host) hosts.push(host.toLowerCase());\n }\n\n return [...new Set(hosts)];\n}\n\nfunction hasReadySetup(cwd: string, entries: DevHostEntry[], configPath: string, https: boolean) {\n const state = readLocalghostState(cwd);\n const projectName = sanitizeProjectName(getProjectName(cwd));\n if (state?.action !== \"setup\" || state.configPath !== configPath) return false;\n\n try {\n const hosts = readFileSync(getSystemHostsPath(), \"utf8\");\n if (!hosts.includes(renderHostsBlock(projectName, entries).trimEnd())) return false;\n } catch {\n return false;\n }\n\n const caddyfilePath = getCaddyfilePath(cwd);\n return existsSync(caddyfilePath) && readFileSync(caddyfilePath, \"utf8\") === renderCaddyfile(entries, { https });\n}\n\nasync function setupProject(cwd: string, entries: DevHostEntry[], configPath: string, https: boolean) {\n const caddy = await checkCaddy();\n if (!caddy.found) {\n throw new Error([\n \"Caddy is missing.\",\n `Run: ${caddy.installHint}`,\n \"Localghost will not install it for you.\"\n ].join(\"\\n\"));\n }\n\n const projectName = sanitizeProjectName(getProjectName(cwd));\n console.log(\"Buh. macOS keeps local hostnames in /etc/hosts, so Localghost may ask for your password.\");\n console.log(\"It will only touch its managed Localghost block.\");\n const hostsResult = await updateSystemHosts(projectName, entries);\n const caddyfilePath = await writeCaddyfile(entries, cwd, { https });\n await validateCaddyfile(caddyfilePath);\n writeLocalghostState(cwd, {\n action: \"setup\",\n projectName,\n cwd,\n configPath,\n hostsPath: hostsResult.hostsPath,\n hostsChanged: hostsResult.changed,\n ...(hostsResult.tempPath ? { hostsTempPath: hostsResult.tempPath } : {}),\n caddyfilePath,\n caddyHttps: https,\n entries\n });\n}\n\nasync function ensureLocalghostContext(options: LocalGhostPluginOptions, vitePort: number, https: boolean) {\n const cwd = options.cwd ?? process.cwd();\n const readOptions = readOptionsFromPlugin(options);\n const resolved = resolveDevHostsPath(readOptions);\n\n if (!resolved.exists) {\n if (options.setup === false || !canPrompt()) {\n throw new Error(\n `No .localghost found at ${resolved.path}. Run \\`localghost init --write-scripts\\` or start Vite in an interactive terminal.`\n );\n }\n\n console.log(`No .localghost found at ${resolved.path}.`);\n if (!(await confirm(\"Create one now?\", true))) {\n throw new Error(\"Localghost setup skipped. Create .localghost before running the Vite plugin.\");\n }\n\n const hosts = await promptForHosts(cwd, vitePort);\n writeTextFile(resolved.path, renderConfig(hosts, vitePort));\n console.log(`Created ${resolved.path}`);\n }\n\n const context = await resolveLocalghostContext({\n ...options,\n cwd,\n port: vitePort,\n https\n });\n\n if (!hasReadySetup(cwd, context.entries, resolved.path, https)) {\n if (options.setup === false || !canPrompt()) return context;\n\n const setup = await confirm(\"Run caddy:setup now?\", true);\n if (setup) {\n await setupProject(cwd, context.entries, resolved.path, https);\n console.log(`All set. Setup state: ${getLocalghostStatePath(cwd)}`);\n }\n }\n\n return context;\n}\n\nexport function localGhostPlugin(options: LocalGhostPluginOptions = {}): Plugin {\n let resolvedEntries: DevHostEntry[] = [];\n let resolvedVitePort: number | undefined;\n let restartTimer: NodeJS.Timeout | undefined;\n\n return {\n name: \"localghost:vite\",\n enforce: \"pre\",\n\n async config(userConfig, configEnv: ConfigEnv): Promise<UserConfig> {\n if (configEnv.command !== \"serve\" || configEnv.mode === \"production\" || isProductionLike()) {\n return {};\n }\n\n const existingServer = userConfig.server ?? {};\n const envVitePort = Number.parseInt(process.env.LOCALGHOST_PORT ?? process.env.VITE_PORT ?? \"\", 10);\n const requestedVitePort =\n options.port ??\n existingServer.port ??\n (Number.isInteger(envVitePort) ? envVitePort : 5173);\n const context: LocalghostContext = await ensureLocalghostContext(options, requestedVitePort, Boolean(options.https));\n const entries = context.entries;\n const hosts = context.hosts;\n const primaryHost = context.primaryHost;\n\n resolvedEntries = entries;\n resolvedVitePort = context.port;\n\n const server: ServerOptions = {\n ...existingServer,\n allowedHosts: mergeAllowedHosts(existingServer.allowedHosts, hosts),\n strictPort: existingServer.strictPort ?? true\n };\n\n if (typeof existingServer.host === \"undefined\") {\n server.host = context.bindHost;\n }\n\n if (context.port) {\n server.port = context.port;\n }\n\n if (options.https && primaryHost) {\n const existingWs = typeof server.ws === \"object\" && server.ws ? server.ws : {};\n const existingHmr = typeof existingServer.hmr === \"object\" && existingServer.hmr ? existingServer.hmr : {};\n\n server.ws = {\n ...existingWs,\n protocol: \"wss\",\n host: primaryHost,\n clientPort: 443\n } satisfies WsOptions;\n\n server.hmr = {\n ...existingHmr,\n protocol: \"wss\",\n host: primaryHost,\n clientPort: 443\n } satisfies HmrOptions;\n }\n\n return { server };\n },\n\n configureServer(server) {\n const watchFiles = getConfigWatchFiles(options);\n const watchedConfigFiles = new Set(watchFiles.map(normalizeWatchPath));\n\n server.watcher.add(watchFiles);\n\n const restartOnLocalghostConfigChange = (filePath: string) => {\n if (!watchedConfigFiles.has(normalizeWatchPath(filePath))) {\n return;\n }\n\n if (restartTimer) {\n clearTimeout(restartTimer);\n }\n\n restartTimer = setTimeout(() => {\n if (options.log !== false) {\n server.config.logger.info(\"localghost config changed; restarting Vite dev server\", {\n clear: false,\n timestamp: false\n });\n }\n\n void server.restart().catch((error: unknown) => {\n server.config.logger.error(error instanceof Error ? error.message : String(error), {\n timestamp: false\n });\n });\n }, 50);\n };\n\n server.watcher.on(\"add\", restartOnLocalghostConfigChange);\n server.watcher.on(\"change\", restartOnLocalghostConfigChange);\n server.watcher.on(\"unlink\", restartOnLocalghostConfigChange);\n\n if (options.log !== false) {\n server.printUrls = () => {\n printLocalHosts(server, resolvedEntries, resolvedVitePort, Boolean(options.https));\n };\n }\n }\n };\n}\n\nexport const localHostsPlugin = localGhostPlugin;\nexport type LocalHostsPluginOptions = LocalGhostPluginOptions;\n","import { existsSync, readFileSync, readdirSync } from \"node:fs\";\nimport { basename, join, resolve } from \"node:path\";\nimport { parseDevHosts } from \"./parse.js\";\n\nexport const LOCALGHOST_CONFIG_FILE = \".localghost\";\n\nexport type ConfigPattern = string | RegExp;\n\nexport type ReadDevHostsOptions = {\n cwd?: string;\n fileName?: string;\n configFiles?: string[];\n configPattern?: ConfigPattern;\n};\n\nexport type ResolvedDevHostsPath = {\n path: string;\n fileName: string;\n exists: boolean;\n searchedFiles: string[];\n configPattern?: ConfigPattern;\n};\n\nfunction unique(values: string[]) {\n return [...new Set(values.filter(Boolean))];\n}\n\nfunction toRegExp(pattern: ConfigPattern) {\n return typeof pattern === \"string\" ? new RegExp(pattern) : pattern;\n}\n\nfunction findPatternMatches(cwd: string, pattern: ConfigPattern) {\n const matcher = toRegExp(pattern);\n\n return readdirSync(cwd, { withFileTypes: true })\n .filter((entry) => entry.isFile())\n .map((entry) => entry.name)\n .filter((name) => {\n matcher.lastIndex = 0;\n return matcher.test(name);\n })\n .sort();\n}\n\nexport function getConfigFileCandidates(options: ReadDevHostsOptions = {}) {\n const cwd = options.cwd ?? process.cwd();\n const exactFiles = unique([\n ...(options.fileName ? [options.fileName] : []),\n ...(options.configFiles ?? [])\n ]);\n const patternFiles = options.configPattern ? findPatternMatches(cwd, options.configPattern) : [];\n const candidates = unique([...exactFiles, ...patternFiles]);\n\n if (candidates.length > 0) return candidates;\n if (exactFiles.length > 0 || options.configPattern) return [];\n return [LOCALGHOST_CONFIG_FILE];\n}\n\nexport function resolveDevHostsPath(options: ReadDevHostsOptions = {}): ResolvedDevHostsPath {\n const cwd = options.cwd ?? process.cwd();\n const searchedFiles = getConfigFileCandidates(options);\n\n for (const fileName of searchedFiles) {\n const path = resolve(cwd, fileName);\n if (existsSync(path)) {\n return {\n path,\n fileName: basename(fileName),\n exists: true,\n searchedFiles,\n ...(options.configPattern ? { configPattern: options.configPattern } : {})\n };\n }\n }\n\n const fileName = searchedFiles[0] ?? LOCALGHOST_CONFIG_FILE;\n\n return {\n path: resolve(cwd, fileName),\n fileName: basename(fileName),\n exists: false,\n searchedFiles,\n ...(options.configPattern ? { configPattern: options.configPattern } : {})\n };\n}\n\nexport function getDevHostsPath(options: ReadDevHostsOptions = {}) {\n return resolveDevHostsPath(options).path;\n}\n\nfunction formatSearchedFiles(files: string[], pattern?: ConfigPattern) {\n if (files.length > 0) return files.map((file) => `\\`${file}\\``).join(\", \");\n if (pattern) return `files matching ${pattern.toString()}`;\n return `\\`${LOCALGHOST_CONFIG_FILE}\\``;\n}\n\nexport function readDevHosts(options: ReadDevHostsOptions | string = {}) {\n const resolvedOptions = typeof options === \"string\" ? { cwd: options } : options;\n const resolvedPath = resolveDevHostsPath(resolvedOptions);\n\n if (!resolvedPath.exists) {\n const cwd = resolvedOptions.cwd ?? process.cwd();\n throw new Error(\n `Missing Localghost config in ${cwd}. Looked for ${formatSearchedFiles(resolvedPath.searchedFiles, resolvedPath.configPattern)}. Run \\`localghost init\\` or pass --config/--config-pattern.`\n );\n }\n\n return parseDevHosts(readFileSync(resolvedPath.path, \"utf8\"), resolvedPath.fileName);\n}\n\nexport function getProjectName(cwd = process.cwd()) {\n try {\n const pkg = JSON.parse(readFileSync(join(cwd, \"package.json\"), \"utf8\")) as { name?: unknown };\n const name = typeof pkg.name === \"string\" && pkg.name ? pkg.name : \"app\";\n return sanitizeProjectName(name.replace(/^@/, \"\"));\n } catch {\n return \"app\";\n }\n}\n\nexport function sanitizeProjectName(value: string) {\n const projectName = value.replace(/[^\\w.-]+/g, \"-\").replace(/^-+|-+$/g, \"\");\n return projectName || \"app\";\n}\n","export type DevHostEntry = {\n host: string;\n port: number;\n target: string;\n};\n\nconst HOST_PATTERN = /^(?=.{1,253}$)(?!-)[a-z0-9-]+(\\.[a-z0-9-]+)*\\.?$/i;\n\nexport function parseDevHosts(input: string, fileName = \".localghost\"): DevHostEntry[] {\n const entries: DevHostEntry[] = [];\n\n input.split(/\\r?\\n/).forEach((rawLine, index) => {\n const line = rawLine.replace(/#.*/, \"\").trim();\n\n if (!line) {\n return;\n }\n\n const parts = line.split(/\\s+/);\n const host = parts[0];\n const portRaw = parts[1];\n\n if (!host || !portRaw || parts.length > 2) {\n throw new Error(`Invalid ${fileName} line ${index + 1}: \"${rawLine}\"`);\n }\n\n if (!HOST_PATTERN.test(host)) {\n throw new Error(`Invalid host on line ${index + 1}: \"${host}\"`);\n }\n\n const port = Number(portRaw);\n\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw new Error(`Invalid port on line ${index + 1}: \"${portRaw}\"`);\n }\n\n entries.push({\n host: host.toLowerCase().replace(/\\.$/, \"\"),\n port,\n target: `127.0.0.1:${port}`\n });\n });\n\n return entries;\n}\n\nexport function findLocalMdnsHosts(entries: DevHostEntry[]): string[] {\n return [...new Set(entries.map((entry) => entry.host).filter((host) => host.endsWith(\".local\")))];\n}\n","import { createServer } from \"node:net\";\n\nexport type FindAvailablePortOptions = {\n host?: string;\n maxAttempts?: number;\n};\n\nexport async function isPortAvailable(port: number, host = \"127.0.0.1\") {\n return new Promise<boolean>((resolve) => {\n const server = createServer();\n\n server.once(\"error\", () => {\n resolve(false);\n });\n\n server.once(\"listening\", () => {\n server.close(() => resolve(true));\n });\n\n server.listen(port, host);\n });\n}\n\nexport async function findAvailablePort(startPort: number, options: FindAvailablePortOptions = {}) {\n const host = options.host ?? \"127.0.0.1\";\n const maxAttempts = options.maxAttempts ?? 50;\n\n for (let offset = 0; offset < maxAttempts; offset += 1) {\n const port = startPort + offset;\n if (await isPortAvailable(port, host)) {\n return port;\n }\n }\n\n throw new Error(`No available port found from ${startPort} to ${startPort + maxAttempts - 1}.`);\n}\n","import {\n getProjectName,\n readDevHosts,\n resolveDevHostsPath,\n sanitizeProjectName,\n type ConfigPattern,\n type ReadDevHostsOptions\n} from \"./config.js\";\nimport { findAvailablePort } from \"./port.js\";\nimport type { DevHostEntry } from \"./parse.js\";\n\nexport type LocalghostContextOptions = {\n cwd?: string;\n project?: string;\n fileName?: string;\n configFiles?: string[];\n configPattern?: ConfigPattern;\n port?: number;\n https?: boolean;\n bindHost?: string | boolean;\n primaryHost?: string;\n dynamicPort?: boolean;\n};\n\nexport type LocalghostContext = {\n cwd: string;\n projectName: string;\n readOptions: ReadDevHostsOptions;\n configPath: string;\n configFileName: string;\n configEntries: DevHostEntry[];\n entries: DevHostEntry[];\n hosts: string[];\n requestedPort: number;\n port: number;\n dynamicPort: boolean;\n bindHost: string | boolean;\n primaryHost: string;\n https: boolean;\n};\n\nfunction parsePort(value: string | undefined) {\n if (!value) return undefined;\n const port = Number.parseInt(value, 10);\n return Number.isInteger(port) && port > 0 && port <= 65535 ? port : undefined;\n}\n\nfunction envPort() {\n return parsePort(process.env.LOCALGHOST_PORT) ?? parsePort(process.env.VITE_PORT);\n}\n\nfunction envDynamicPort() {\n const value = process.env.LOCALGHOST_DYNAMIC_PORT;\n if (!value) return undefined;\n return [\"1\", \"true\", \"yes\", \"on\"].includes(value.toLowerCase());\n}\n\nfunction readOptionsFromContext(options: LocalghostContextOptions): ReadDevHostsOptions {\n return {\n cwd: options.cwd ?? process.cwd(),\n ...(options.fileName ? { fileName: options.fileName } : {}),\n ...(options.configFiles ? { configFiles: options.configFiles } : {}),\n ...(options.configPattern ? { configPattern: options.configPattern } : {})\n };\n}\n\nfunction withRuntimePort(entries: DevHostEntry[], requestedPort: number, port: number) {\n if (requestedPort === port) return entries;\n\n const hasRequestedPort = entries.some((entry) => entry.port === requestedPort);\n if (!hasRequestedPort) return entries;\n\n return entries.map((entry) => (entry.port === requestedPort ? { ...entry, port } : entry));\n}\n\nfunction uniqueHosts(entries: DevHostEntry[]) {\n return [...new Set(entries.map((entry) => entry.host))];\n}\n\nexport function defineLocalghostConfig<T extends LocalghostContextOptions>(config: T) {\n return config;\n}\n\nexport async function resolveLocalghostContext(options: LocalghostContextOptions = {}): Promise<LocalghostContext> {\n const cwd = options.cwd ?? process.cwd();\n const readOptions = readOptionsFromContext({ ...options, cwd });\n const resolvedPath = resolveDevHostsPath(readOptions);\n const configEntries = readDevHosts(readOptions);\n const requestedPort = options.port ?? envPort() ?? configEntries[0]?.port ?? 5173;\n const dynamicPort = options.dynamicPort ?? envDynamicPort() ?? false;\n const bindHost = options.bindHost ?? \"127.0.0.1\";\n const probeHost = typeof bindHost === \"string\" ? bindHost : \"127.0.0.1\";\n const port = dynamicPort ? await findAvailablePort(requestedPort, { host: probeHost }) : requestedPort;\n const entries = withRuntimePort(configEntries, requestedPort, port);\n const hosts = uniqueHosts(entries);\n const primaryHost =\n options.primaryHost ??\n entries.find((entry) => entry.port === port)?.host ??\n hosts[0] ??\n `${sanitizeProjectName(getProjectName(cwd))}.localhost`;\n\n return {\n cwd,\n projectName: sanitizeProjectName(options.project ?? getProjectName(cwd)),\n readOptions,\n configPath: resolvedPath.path,\n configFileName: resolvedPath.fileName,\n configEntries,\n entries,\n hosts,\n requestedPort,\n port,\n dynamicPort,\n bindHost,\n primaryHost,\n https: options.https === true\n };\n}\n","import { execa } from \"execa\";\n\nexport type DoctorResult = {\n ok: boolean;\n caddy: {\n found: boolean;\n version?: string;\n installHint: string;\n };\n};\n\nexport async function checkCaddy(): Promise<DoctorResult[\"caddy\"]> {\n try {\n const result = await execa(\"caddy\", [\"version\"], { reject: false });\n const version = [result.stdout, result.stderr].filter(Boolean).join(\"\\n\").trim();\n\n return {\n found: result.exitCode === 0,\n ...(version ? { version } : {}),\n installHint: \"brew install caddy\"\n };\n } catch {\n return {\n found: false,\n installHint: \"brew install caddy\"\n };\n }\n}\n\nexport async function runDoctor(): Promise<DoctorResult> {\n const caddy = await checkCaddy();\n return {\n ok: caddy.found,\n caddy\n };\n}\n","export type LocalghostEnvironment = NodeJS.ProcessEnv;\n\nconst PRODUCTION_ENV_KEYS = [\"NODE_ENV\", \"VERCEL_ENV\", \"NETLIFY\", \"CF_PAGES_BRANCH\", \"LOCALGHOST_ENV\"] as const;\n\nexport function getProductionReason(env: LocalghostEnvironment = process.env) {\n if (env.LOCALGHOST_ENV === \"production\") return \"LOCALGHOST_ENV=production\";\n if (env.NODE_ENV === \"production\") return \"NODE_ENV=production\";\n if (env.VERCEL_ENV === \"production\") return \"VERCEL_ENV=production\";\n if (env.NETLIFY === \"true\" && env.CONTEXT === \"production\") return \"NETLIFY=true and CONTEXT=production\";\n if (env.CF_PAGES_BRANCH && env.CF_PAGES_BRANCH === env.CF_PAGES_PRODUCTION_BRANCH) {\n return \"CF_PAGES_BRANCH matches CF_PAGES_PRODUCTION_BRANCH\";\n }\n\n return null;\n}\n\nexport function isProductionLike(env: LocalghostEnvironment = process.env) {\n return getProductionReason(env) !== null;\n}\n\nexport function assertLocalDevelopment(command: string, env: LocalghostEnvironment = process.env) {\n const reason = getProductionReason(env);\n if (!reason) return;\n\n throw new Error(`Localghost only runs in local development. Refusing \\`${command}\\` because ${reason}.`);\n}\n\nexport function getProductionEnvKeys() {\n return PRODUCTION_ENV_KEYS;\n}\n","import { mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\n\nexport function readTextFile(path: string) {\n return readFileSync(path, \"utf8\");\n}\n\nexport function writeTextFile(path: string, value: string) {\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, value, \"utf8\");\n return path;\n}\n","import { writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { execa } from \"execa\";\nimport { sanitizeProjectName } from \"./config.js\";\nimport { readTextFile } from \"./fs.js\";\nimport type { DevHostEntry } from \"./parse.js\";\n\nexport type UpdateSystemHostsResult = {\n changed: boolean;\n hostsPath: string;\n tempPath?: string;\n};\n\nexport type RemoveSystemHostsResult = UpdateSystemHostsResult & {\n removed: boolean;\n};\n\nfunction escapeRegExp(value: string) {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nfunction getManagedBlockPattern(projectName: string) {\n const sanitizedProjectName = sanitizeProjectName(projectName);\n const start = `# localghost:start ${sanitizedProjectName}`;\n const end = `# localghost:end ${sanitizedProjectName}`;\n return new RegExp(`${escapeRegExp(start)}[\\\\s\\\\S]*?${escapeRegExp(end)}\\\\n?`, \"m\");\n}\n\nexport function getSystemHostsPath() {\n return process.platform === \"win32\" ? \"C:\\\\Windows\\\\System32\\\\drivers\\\\etc\\\\hosts\" : \"/etc/hosts\";\n}\n\nexport function renderHostsBlock(projectName: string, entries: DevHostEntry[]) {\n const sanitizedProjectName = sanitizeProjectName(projectName);\n const hosts = [...new Set(entries.map((entry) => entry.host))].sort();\n\n return [\n `# localghost:start ${sanitizedProjectName}`,\n ...hosts.map((host) => `127.0.0.1 ${host}`),\n `# localghost:end ${sanitizedProjectName}`,\n \"\"\n ].join(\"\\n\");\n}\n\nexport function upsertManagedBlock(existing: string, projectName: string, block: string) {\n const pattern = getManagedBlockPattern(projectName);\n\n if (pattern.test(existing)) {\n return existing.replace(pattern, block);\n }\n\n return `${existing.trimEnd()}\\n\\n${block}`;\n}\n\nexport function removeManagedBlock(existing: string, projectName: string) {\n const pattern = getManagedBlockPattern(projectName);\n\n if (!pattern.test(existing)) {\n return existing;\n }\n\n return existing.replace(pattern, \"\").replace(/\\n{3,}/g, \"\\n\\n\").trimEnd() + \"\\n\";\n}\n\nasync function writeSystemHostsFile(hostsPath: string, next: string, projectName: string) {\n const sanitizedProjectName = sanitizeProjectName(projectName);\n const tempPath = join(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);\n writeFileSync(tempPath, next, \"utf8\");\n\n if (process.platform === \"win32\") {\n throw new Error(`Windows support: run as administrator and copy ${tempPath} to ${hostsPath}.`);\n }\n\n await execa(\"sudo\", [\"cp\", tempPath, hostsPath], { stdio: \"inherit\" });\n\n return tempPath;\n}\n\nexport async function updateSystemHosts(projectName: string, entries: DevHostEntry[]): Promise<UpdateSystemHostsResult> {\n const sanitizedProjectName = sanitizeProjectName(projectName);\n const hostsPath = getSystemHostsPath();\n const existing = readTextFile(hostsPath);\n const block = renderHostsBlock(sanitizedProjectName, entries);\n const next = upsertManagedBlock(existing, sanitizedProjectName, block);\n\n if (next === existing) {\n return { changed: false, hostsPath };\n }\n\n const tempPath = await writeSystemHostsFile(hostsPath, next, sanitizedProjectName);\n\n return { changed: true, hostsPath, tempPath };\n}\n\nexport async function removeSystemHosts(projectName: string): Promise<RemoveSystemHostsResult> {\n const sanitizedProjectName = sanitizeProjectName(projectName);\n const hostsPath = getSystemHostsPath();\n const existing = readTextFile(hostsPath);\n const next = removeManagedBlock(existing, sanitizedProjectName);\n\n if (next === existing) {\n return { changed: false, removed: false, hostsPath };\n }\n\n const tempPath = await writeSystemHostsFile(hostsPath, next, sanitizedProjectName);\n\n return { changed: true, removed: true, hostsPath, tempPath };\n}\n","import { stdin as input, stdout as output } from \"node:process\";\nimport { createInterface } from \"node:readline/promises\";\n\nexport function canPrompt() {\n return Boolean(input.isTTY && output.isTTY);\n}\n\nexport async function withPrompt<T>(run: (prompt: (question: string) => Promise<string>) => Promise<T>) {\n const rl = createInterface({ input, output });\n try {\n return await run((question) => rl.question(question));\n } finally {\n rl.close();\n }\n}\n\nexport async function confirm(question: string, defaultValue = true) {\n return withPrompt(async (prompt) => {\n const suffix = defaultValue ? \" [Y/n] \" : \" [y/N] \";\n const answer = (await prompt(`${question}${suffix}`)).trim().toLowerCase();\n if (!answer) return defaultValue;\n return answer === \"y\" || answer === \"yes\";\n });\n}\n\nexport async function ask(question: string, defaultValue?: string) {\n return withPrompt(async (prompt) => {\n const suffix = defaultValue ? ` (${defaultValue}) ` : \" \";\n const answer = (await prompt(`${question}${suffix}`)).trim();\n return answer || defaultValue || \"\";\n });\n}\n","import { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { readTextFile, writeTextFile } from \"./fs.js\";\nimport type { DevHostEntry } from \"./parse.js\";\n\nexport const LOCALGHOST_STATE_FILE = \"ops/local/localghost-state.json\";\n\nexport type LocalghostStateAction = \"setup\" | \"teardown\";\n\nexport type LocalghostState = {\n version: 1;\n action: LocalghostStateAction;\n updatedAt: string;\n projectName: string;\n cwd: string;\n configPath?: string;\n hostsPath?: string;\n hostsChanged?: boolean;\n hostsTempPath?: string;\n caddyfilePath?: string;\n caddyfileRemoved?: boolean;\n caddyHttps?: boolean;\n entries?: DevHostEntry[];\n};\n\nexport type WriteLocalghostStateInput = Omit<LocalghostState, \"version\" | \"updatedAt\">;\n\nexport function getLocalghostStatePath(cwd = process.cwd()) {\n return join(cwd, LOCALGHOST_STATE_FILE);\n}\n\nexport function readLocalghostState(cwd = process.cwd()): LocalghostState | null {\n const path = getLocalghostStatePath(cwd);\n if (!existsSync(path)) return null;\n return JSON.parse(readTextFile(path)) as LocalghostState;\n}\n\nexport function writeLocalghostState(cwd: string, state: WriteLocalghostStateInput) {\n const path = getLocalghostStatePath(cwd);\n writeTextFile(path, `${JSON.stringify({ version: 1, updatedAt: new Date().toISOString(), ...state }, null, 2)}\\n`);\n return path;\n}\n","import { dirname, join } from \"node:path\";\nimport { execa } from \"execa\";\nimport { writeTextFile } from \"./fs.js\";\nimport type { DevHostEntry } from \"./parse.js\";\n\nexport type CaddyModeOptions = {\n https?: boolean;\n};\n\nfunction groupByPort(entries: DevHostEntry[]) {\n const groups = new Map<number, DevHostEntry[]>();\n\n for (const entry of entries) {\n const group = groups.get(entry.port) ?? [];\n group.push(entry);\n groups.set(entry.port, group);\n }\n\n return groups;\n}\n\nexport function getCaddyfilePath(cwd = process.cwd()) {\n return join(cwd, \"ops/local/Caddyfile\");\n}\n\nexport function renderCaddyfile(entries: DevHostEntry[], options: CaddyModeOptions = {}) {\n const groups = groupByPort(entries);\n const https = options.https === true;\n const blocks = [...groups.entries()]\n .sort(([leftPort], [rightPort]) => leftPort - rightPort)\n .map(([port, group]) => {\n const hosts = group\n .map((entry) => (https ? entry.host : `http://${entry.host}`))\n .sort()\n .join(\", \");\n\n return `${hosts} {\n reverse_proxy 127.0.0.1:${port}\n}`;\n });\n\n const globalOptions = https\n ? `{\n local_certs\n}\n\n`\n : \"\";\n\n return `${globalOptions}${blocks.join(\"\\n\\n\")}\n`;\n}\n\nexport async function writeCaddyfile(entries: DevHostEntry[], cwd = process.cwd(), options: CaddyModeOptions = {}) {\n const path = getCaddyfilePath(cwd);\n writeTextFile(path, renderCaddyfile(entries, options));\n return path;\n}\n\nexport async function validateCaddyfile(path: string) {\n await execa(\"caddy\", [\"validate\", \"--config\", path], {\n cwd: dirname(path),\n stdio: \"inherit\"\n });\n}\n\nexport async function runCaddy(path: string) {\n await execa(\"caddy\", [\"run\", \"--config\", path], {\n cwd: dirname(path),\n stdio: \"inherit\"\n });\n}\n\nexport function startCaddy(path: string) {\n return execa(\"caddy\", [\"run\", \"--config\", path], {\n cwd: dirname(path),\n stdio: \"inherit\"\n });\n}\n"],"mappings":"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","names":["existsSync","readFileSync","resolve","input","fileName","resolve","readFileSync","writeFileSync","join","execa","join","writeFileSync","execa","existsSync","join","join","existsSync","dirname","join","execa","join","execa","dirname","resolve","readFileSync","existsSync"]}
|
package/docs/flows.md
CHANGED
|
@@ -58,14 +58,21 @@ yarn localghost:setup
|
|
|
58
58
|
|
|
59
59
|
## Daily Dev
|
|
60
60
|
|
|
61
|
-
As a developer, I want a daily command that starts the local
|
|
61
|
+
As a developer, I want a daily command that starts the local HTTP proxy from the same config file, with local HTTPS available only when I ask for it.
|
|
62
62
|
|
|
63
63
|
```sh
|
|
64
|
+
yarn localghost:ready
|
|
64
65
|
yarn localghost:proxy
|
|
65
66
|
```
|
|
66
67
|
|
|
67
68
|
Most repos will run this next to their app server, for example Vite on `127.0.0.1:5173`.
|
|
68
69
|
|
|
70
|
+
When a repo really needs local certificates:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
yarn localghost:proxy:https
|
|
74
|
+
```
|
|
75
|
+
|
|
69
76
|
## Config Discovery
|
|
70
77
|
|
|
71
78
|
As a developer, I want Localghost to fit repos that already have naming conventions without hidden file searches.
|
|
@@ -88,24 +95,39 @@ yarn localghost routes
|
|
|
88
95
|
|
|
89
96
|
```txt
|
|
90
97
|
localghost routes
|
|
91
|
-
|
|
92
|
-
|
|
98
|
+
http://app.localhost/ -> http://127.0.0.1:5173
|
|
99
|
+
http://api.app.localhost/ -> http://127.0.0.1:8787
|
|
93
100
|
```
|
|
94
101
|
|
|
95
102
|
`setup` and `dev` print this same map before Caddy is validated or run.
|
|
96
103
|
|
|
97
104
|
## Vite Integration
|
|
98
105
|
|
|
99
|
-
As a Vite user, I want Localghost to set strict `allowedHosts
|
|
106
|
+
As a Vite user, I want Localghost to set the dev host to my configured domain, keep strict `allowedHosts`, print browser-facing URLs, and never run in production/build mode.
|
|
100
107
|
|
|
101
108
|
```ts
|
|
102
109
|
import { localGhostPlugin } from "@hamedb89/localghost/vite";
|
|
103
110
|
|
|
104
111
|
export default {
|
|
105
|
-
plugins: [localGhostPlugin({ port: 5173
|
|
112
|
+
plugins: [localGhostPlugin({ port: 5173 })]
|
|
106
113
|
};
|
|
107
114
|
```
|
|
108
115
|
|
|
116
|
+
The plugin defaults to HTTP. Pass `https: true` only when Vite is expected to sit behind a Caddy HTTPS proxy. Localghost prints URLs but does not open browser tabs.
|
|
117
|
+
|
|
118
|
+
If `.localghost` is missing, an interactive `yarn dev` asks whether to create it, asks for the primary `.localhost` domain, allows extra domains, explains the `/etc/hosts` password prompt, and runs setup when confirmed. Non-interactive runs fail with the exact setup command instead of guessing.
|
|
119
|
+
|
|
120
|
+
## Reset For Testing
|
|
121
|
+
|
|
122
|
+
As a developer, I want to retest setup without deleting my project config.
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
yarn localghost reset
|
|
126
|
+
yarn localghost setup
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`reset` removes only the managed hosts block, generated Caddyfile, and setup state. It leaves `.localghost` in place.
|
|
130
|
+
|
|
109
131
|
## Teardown
|
|
110
132
|
|
|
111
133
|
As a developer, I want to cleanly remove Localghost from a project when the repo is archived or no longer needs friendly hostnames.
|
package/docs/github.md
CHANGED
|
@@ -4,11 +4,11 @@ Use this copy for the GitHub repo About box, topics, and social cards. Keep it s
|
|
|
4
4
|
|
|
5
5
|
## Repository Description
|
|
6
6
|
|
|
7
|
-
Friendly local
|
|
7
|
+
Friendly local hostnames for app repos. A tiny CLI for `.localghost` configs, `/etc/hosts` blocks, Caddy reverse proxies, and Vite `allowedHosts`.
|
|
8
8
|
|
|
9
9
|
Shorter alternative:
|
|
10
10
|
|
|
11
|
-
Friendly local
|
|
11
|
+
Friendly local hostnames for app repos, powered by `.localghost`, Caddy, `/etc/hosts`, and Vite.
|
|
12
12
|
|
|
13
13
|
## Topics
|
|
14
14
|
|
|
@@ -49,7 +49,7 @@ reverse-proxy
|
|
|
49
49
|
|
|
50
50
|
## Search Phrases To Own
|
|
51
51
|
|
|
52
|
-
- local
|
|
52
|
+
- local hostnames for Vite
|
|
53
53
|
- Caddy localhost reverse proxy
|
|
54
54
|
- friendly localhost domains
|
|
55
55
|
- manage /etc/hosts for local development
|
|
@@ -62,7 +62,7 @@ After creating `hamedb89/localghost`, this sets the public repo metadata:
|
|
|
62
62
|
|
|
63
63
|
```sh
|
|
64
64
|
gh repo edit hamedb89/localghost \
|
|
65
|
-
--description "Friendly local
|
|
65
|
+
--description "Friendly local hostnames for app repos. A tiny CLI for .localghost configs, /etc/hosts blocks, Caddy reverse proxies, and Vite allowedHosts." \
|
|
66
66
|
--homepage "https://hamedb89.github.io/localghost/" \
|
|
67
67
|
--add-topic localhost \
|
|
68
68
|
--add-topic local-development \
|
|
@@ -81,7 +81,7 @@ gh repo edit hamedb89/localghost \
|
|
|
81
81
|
The first visible paragraph should say what it is, who it is for, and what tools it touches:
|
|
82
82
|
|
|
83
83
|
```txt
|
|
84
|
-
Localghost is a tiny Node.js CLI for local
|
|
84
|
+
Localghost is a tiny Node.js CLI for local domains in app repos. It gives each project one small contract for `.localhost` hostnames, Caddy reverse proxies, Vite `allowedHosts`, and the system hosts file.
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
That phrasing helps GitHub search and npm search without making the README feel like SEO sludge.
|
|
@@ -120,10 +120,48 @@ npm run release:check
|
|
|
120
120
|
|
|
121
121
|
That command typechecks, builds the package, builds the static site, and runs `npm pack --dry-run`.
|
|
122
122
|
|
|
123
|
-
`.github/workflows/publish-npm.yml` publishes to npm
|
|
123
|
+
`.github/workflows/publish-npm.yml` publishes to npm when a `v*` tag is pushed, or from manual workflow dispatch. The workflow checks that tag names match `package.json` versions, reruns `npm run release:check`, and then uses:
|
|
124
124
|
|
|
125
125
|
```sh
|
|
126
|
-
npm publish
|
|
126
|
+
npm run publish:public
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
That package script runs `npm publish --access public --provenance`.
|
|
130
|
+
|
|
131
|
+
Configure npm trusted publishing for `hamedb89/localghost` before relying on the release workflow. On npmjs.com, open the package settings and add a trusted publisher with:
|
|
132
|
+
|
|
133
|
+
```txt
|
|
134
|
+
Provider: GitHub Actions
|
|
135
|
+
Organization or user: hamedb89
|
|
136
|
+
Repository: localghost
|
|
137
|
+
Workflow filename: publish-npm.yml
|
|
138
|
+
Environment name: npm
|
|
139
|
+
Allowed actions: npm publish
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The workflow must keep `id-token: write`, run on a GitHub-hosted runner, and use a recent npm CLI. Trusted publishing does not need an `NPM_TOKEN` secret. npm automatically generates provenance for public packages published from public GitHub repositories through trusted publishing.
|
|
143
|
+
|
|
144
|
+
If the workflow reaches `npm publish` and npm returns `404 Not Found` or a permission-flavored 404 for `@hamedb89/localghost`, recheck the trusted publisher fields above. The package, repository, workflow filename, environment name, and allowed action must match exactly.
|
|
145
|
+
|
|
146
|
+
Local manual publishes are guarded by the `prepublishOnly` package hook, which runs the same release check.
|
|
147
|
+
|
|
148
|
+
## Patch Release Workflow
|
|
149
|
+
|
|
150
|
+
Use patch releases for docs, packaging metadata, small fixes, and backwards-compatible CLI/API changes. Do not overwrite a published npm version; npm package versions are immutable, and git tags should continue to identify the source that produced that published package.
|
|
151
|
+
|
|
152
|
+
For a normal patch release:
|
|
153
|
+
|
|
154
|
+
```sh
|
|
155
|
+
git status --short
|
|
156
|
+
npm run release:check
|
|
157
|
+
npm version patch
|
|
158
|
+
git push origin main --tags
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Pushing the `v*` tag starts the npm publish workflow automatically. The workflow reruns `npm run release:check` before publishing. Create the GitHub release afterwards if you want release notes on GitHub.
|
|
162
|
+
|
|
163
|
+
If publishing from a local terminal instead of GitHub Actions, omit provenance and provide the npm two-factor code:
|
|
164
|
+
|
|
165
|
+
```sh
|
|
166
|
+
npm publish --access public --otp=123456
|
|
167
|
+
```
|
package/docs/localghost.1.md
CHANGED
|
@@ -9,20 +9,25 @@ localghost - friendly local hostnames for app repos
|
|
|
9
9
|
```sh
|
|
10
10
|
localghost init [--write-scripts] [--config file] [--host host] [--port port]
|
|
11
11
|
localghost doctor
|
|
12
|
-
localghost setup [--project name] [--config file] [--config-pattern regex]
|
|
12
|
+
localghost setup [--project name] [--config file] [--config-pattern regex] [--https|--ssl]
|
|
13
|
+
localghost reset [--project name]
|
|
13
14
|
localghost teardown [--project name] [--remove-caddyfile]
|
|
14
|
-
localghost status [--json]
|
|
15
|
+
localghost status [--ready] [--json]
|
|
16
|
+
localghost ps [--json]
|
|
15
17
|
localghost update [--json]
|
|
16
|
-
localghost dev [--config file] [--config-pattern regex]
|
|
18
|
+
localghost dev [--config file] [--config-pattern regex] [--https|--ssl] [--setup]
|
|
19
|
+
localghost run [--config file] [--config-pattern regex] [--https|--ssl] [--setup] [--dynamic-port] -- command
|
|
17
20
|
localghost print [--config file] [--config-pattern regex]
|
|
18
21
|
```
|
|
19
22
|
|
|
20
23
|
## Description
|
|
21
24
|
|
|
22
|
-
Localghost reads `.localghost`, writes a managed `/etc/hosts` block, records `ops/local/localghost-state.json`, generates `ops/local/Caddyfile`, and runs a Caddy local
|
|
25
|
+
Localghost reads `.localghost`, writes a managed `/etc/hosts` block, records `ops/local/localghost-state.json`, generates `ops/local/Caddyfile`, and runs a Caddy local proxy. HTTP is the default; local HTTPS is explicit with `--https` or `--ssl`. It is intentionally small and explicit: no hidden installs, no full hosts-file rewrites, no surprise browser tabs, and no broad Vite `allowedHosts: true` shortcut.
|
|
23
26
|
|
|
24
27
|
Localghost checks npm for newer releases after successful commands. The check is best-effort, cached for 24 hours, and can be disabled with `LOCALGHOST_NO_UPDATE_CHECK=1` or `--no-update-check`.
|
|
25
28
|
|
|
29
|
+
`setup`, `dev`, and `teardown` refuse to run in production-like environments such as `NODE_ENV=production`, `VERCEL_ENV=production`, or `LOCALGHOST_ENV=production`.
|
|
30
|
+
|
|
26
31
|
## Commands
|
|
27
32
|
|
|
28
33
|
### init
|
|
@@ -56,7 +61,7 @@ Currently checks Caddy and prints `brew install caddy` when missing.
|
|
|
56
61
|
|
|
57
62
|
### setup
|
|
58
63
|
|
|
59
|
-
Updates the managed Localghost block in `/etc/hosts`, writes `ops/local/Caddyfile`, and validates it with Caddy. Pass `--config <file>` to look for a specific config file. Repeat `--config` to use the first existing file from an ordered list. Pass `--config-pattern <regex>` to search matching filenames in the project root.
|
|
64
|
+
Updates the managed Localghost block in `/etc/hosts`, writes `ops/local/Caddyfile`, and validates it with Caddy. Pass `--config <file>` to look for a specific config file. Repeat `--config` to use the first existing file from an ordered list. Pass `--config-pattern <regex>` to search matching filenames in the project root. Pass `--https` or `--ssl` to generate a local HTTPS Caddyfile.
|
|
60
65
|
|
|
61
66
|
```sh
|
|
62
67
|
localghost setup --project app
|
|
@@ -70,12 +75,30 @@ Removes the managed Localghost block from `/etc/hosts` for the selected project
|
|
|
70
75
|
localghost teardown --remove-caddyfile
|
|
71
76
|
```
|
|
72
77
|
|
|
78
|
+
### reset
|
|
79
|
+
|
|
80
|
+
Removes the managed Localghost hosts block, generated Caddyfile, and setup state, but keeps `.localghost` in place so setup can be tested again.
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
localghost reset
|
|
84
|
+
localghost setup
|
|
85
|
+
```
|
|
86
|
+
|
|
73
87
|
### status
|
|
74
88
|
|
|
75
|
-
Prints Localghost's project-local state file. Pass `--json` for scripts and agents.
|
|
89
|
+
Prints Localghost's project-local state file and setup readiness. Pass `--ready` to exit non-zero when the hosts block or Caddyfile is missing or stale. Pass `--json` for scripts and agents.
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
localghost status --ready
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### ps
|
|
96
|
+
|
|
97
|
+
Shows Localghost `dev` and `run` sessions that are currently running on the machine. Stale records are pruned automatically when their wrapper process is gone. Each route also reports whether its upstream `127.0.0.1:<port>` is listening. Pass `--json` for menu bar helpers or other polling tools.
|
|
76
98
|
|
|
77
99
|
```sh
|
|
78
|
-
localghost
|
|
100
|
+
localghost ps
|
|
101
|
+
localghost ps --json
|
|
79
102
|
```
|
|
80
103
|
|
|
81
104
|
### update
|
|
@@ -88,7 +111,7 @@ localghost update
|
|
|
88
111
|
|
|
89
112
|
### routes
|
|
90
113
|
|
|
91
|
-
Prints the local domain layer as `domain -> upstream` routes. Pass `--
|
|
114
|
+
Prints the local domain layer as `domain -> upstream` routes. HTTP is the default. Pass `--https` or `--ssl` if the browser-facing domain should be shown as HTTPS.
|
|
92
115
|
|
|
93
116
|
```sh
|
|
94
117
|
localghost routes
|
|
@@ -96,12 +119,25 @@ localghost routes
|
|
|
96
119
|
|
|
97
120
|
### dev
|
|
98
121
|
|
|
99
|
-
|
|
122
|
+
Requires setup to be ready, writes `ops/local/Caddyfile`, validates it, and runs Caddy. Supports `--config` and `--config-pattern`. HTTP is the default. Pass `--https` or `--ssl` to run a local HTTPS proxy. Pass `--setup` to explicitly allow `dev` to run setup first when setup is missing or stale.
|
|
100
123
|
|
|
101
124
|
```sh
|
|
102
125
|
localghost dev
|
|
103
126
|
```
|
|
104
127
|
|
|
128
|
+
### run
|
|
129
|
+
|
|
130
|
+
Resolves one Localghost context, ensures setup is ready, writes the runtime Caddyfile, starts Caddy, and runs a child dev command. The selected port is passed to the child as `LOCALGHOST_PORT` and `VITE_PORT`.
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
localghost run -- vite
|
|
134
|
+
localghost run --dynamic-port -- turbo dev
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Pass `--dynamic-port` or `--dynamic-port=yes` to start at the configured port and walk upward until `127.0.0.1:<port>` is free. Pass `--setup` to explicitly allow setup when the hosts block is missing or stale.
|
|
138
|
+
|
|
139
|
+
`dev` and `run` register active sessions in a user-local activity file so `localghost ps` can show what is running across projects.
|
|
140
|
+
|
|
105
141
|
### print
|
|
106
142
|
|
|
107
143
|
Prints parsed Localghost config entries as JSON. Supports `--config` and `--config-pattern`.
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hamedb89/localghost",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Friendly local
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"description": "Friendly local hostnames for app repos with .localghost, Caddy, /etc/hosts, and Vite.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"localghost": "
|
|
7
|
+
"localghost": "dist/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
@@ -46,8 +46,11 @@
|
|
|
46
46
|
"prepack": "npm run build",
|
|
47
47
|
"prepublishOnly": "npm run release:check",
|
|
48
48
|
"pack:dry": "npm pack --dry-run",
|
|
49
|
-
"release:check": "npm run typecheck && npm run build && npm run site:build && npm pack --dry-run",
|
|
49
|
+
"release:check": "npm run version:check && npm run typecheck && npm run build && npm run site:build && npm pack --dry-run",
|
|
50
50
|
"site:build": "node scripts/build-site.mjs",
|
|
51
|
+
"sync:version": "node scripts/sync-readme-version.mjs",
|
|
52
|
+
"version:check": "node scripts/sync-readme-version.mjs --check",
|
|
53
|
+
"version": "npm run sync:version && git add README.md src/update-check.ts",
|
|
51
54
|
"publish:public": "npm publish --access public --provenance"
|
|
52
55
|
},
|
|
53
56
|
"dependencies": {
|