@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/LICENSE +21 -0
- package/README.md +278 -0
- package/assets/localghost-app-icon.png +0 -0
- package/assets/localghost-banner.png +0 -0
- package/assets/localghost-mascot.png +0 -0
- package/assets/localghost-wordmark.png +0 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +804 -0
- package/dist/cli.js.map +1 -0
- package/dist/config-Cde1Bich.d.ts +31 -0
- package/dist/index.d.ts +132 -0
- package/dist/index.js +648 -0
- package/dist/index.js.map +1 -0
- package/dist/vite.d.ts +18 -0
- package/dist/vite.js +202 -0
- package/dist/vite.js.map +1 -0
- package/docs/brand.md +43 -0
- package/docs/flows.md +141 -0
- package/docs/github.md +129 -0
- package/docs/localghost.1.md +124 -0
- package/package.json +92 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { existsSync as existsSync5, unlinkSync } from "fs";
|
|
5
|
+
import { Command, InvalidArgumentError } from "commander";
|
|
6
|
+
|
|
7
|
+
// src/config.ts
|
|
8
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
9
|
+
import { basename, join, resolve } from "path";
|
|
10
|
+
|
|
11
|
+
// src/parse.ts
|
|
12
|
+
var HOST_PATTERN = /^(?=.{1,253}$)(?!-)[a-z0-9-]+(\.[a-z0-9-]+)*\.?$/i;
|
|
13
|
+
function parseDevHosts(input, fileName = ".localghost") {
|
|
14
|
+
const entries = [];
|
|
15
|
+
input.split(/\r?\n/).forEach((rawLine, index) => {
|
|
16
|
+
const line = rawLine.replace(/#.*/, "").trim();
|
|
17
|
+
if (!line) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const parts = line.split(/\s+/);
|
|
21
|
+
const host = parts[0];
|
|
22
|
+
const portRaw = parts[1];
|
|
23
|
+
if (!host || !portRaw || parts.length > 2) {
|
|
24
|
+
throw new Error(`Invalid ${fileName} line ${index + 1}: "${rawLine}"`);
|
|
25
|
+
}
|
|
26
|
+
if (!HOST_PATTERN.test(host)) {
|
|
27
|
+
throw new Error(`Invalid host on line ${index + 1}: "${host}"`);
|
|
28
|
+
}
|
|
29
|
+
const port = Number(portRaw);
|
|
30
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
31
|
+
throw new Error(`Invalid port on line ${index + 1}: "${portRaw}"`);
|
|
32
|
+
}
|
|
33
|
+
entries.push({
|
|
34
|
+
host: host.toLowerCase().replace(/\.$/, ""),
|
|
35
|
+
port,
|
|
36
|
+
target: `127.0.0.1:${port}`
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
return entries;
|
|
40
|
+
}
|
|
41
|
+
function findLocalMdnsHosts(entries) {
|
|
42
|
+
return [...new Set(entries.map((entry) => entry.host).filter((host) => host.endsWith(".local")))];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/config.ts
|
|
46
|
+
var LOCALGHOST_CONFIG_FILE = ".localghost";
|
|
47
|
+
function unique(values) {
|
|
48
|
+
return [...new Set(values.filter(Boolean))];
|
|
49
|
+
}
|
|
50
|
+
function toRegExp(pattern) {
|
|
51
|
+
return typeof pattern === "string" ? new RegExp(pattern) : pattern;
|
|
52
|
+
}
|
|
53
|
+
function findPatternMatches(cwd, pattern) {
|
|
54
|
+
const matcher = toRegExp(pattern);
|
|
55
|
+
return readdirSync(cwd, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => entry.name).filter((name) => {
|
|
56
|
+
matcher.lastIndex = 0;
|
|
57
|
+
return matcher.test(name);
|
|
58
|
+
}).sort();
|
|
59
|
+
}
|
|
60
|
+
function getConfigFileCandidates(options = {}) {
|
|
61
|
+
const cwd = options.cwd ?? process.cwd();
|
|
62
|
+
const exactFiles = unique([
|
|
63
|
+
...options.fileName ? [options.fileName] : [],
|
|
64
|
+
...options.configFiles ?? []
|
|
65
|
+
]);
|
|
66
|
+
const patternFiles = options.configPattern ? findPatternMatches(cwd, options.configPattern) : [];
|
|
67
|
+
const candidates = unique([...exactFiles, ...patternFiles]);
|
|
68
|
+
if (candidates.length > 0) return candidates;
|
|
69
|
+
if (exactFiles.length > 0 || options.configPattern) return [];
|
|
70
|
+
return [LOCALGHOST_CONFIG_FILE];
|
|
71
|
+
}
|
|
72
|
+
function resolveDevHostsPath(options = {}) {
|
|
73
|
+
const cwd = options.cwd ?? process.cwd();
|
|
74
|
+
const searchedFiles = getConfigFileCandidates(options);
|
|
75
|
+
for (const fileName2 of searchedFiles) {
|
|
76
|
+
const path = resolve(cwd, fileName2);
|
|
77
|
+
if (existsSync(path)) {
|
|
78
|
+
return {
|
|
79
|
+
path,
|
|
80
|
+
fileName: basename(fileName2),
|
|
81
|
+
exists: true,
|
|
82
|
+
searchedFiles,
|
|
83
|
+
...options.configPattern ? { configPattern: options.configPattern } : {}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const fileName = searchedFiles[0] ?? LOCALGHOST_CONFIG_FILE;
|
|
88
|
+
return {
|
|
89
|
+
path: resolve(cwd, fileName),
|
|
90
|
+
fileName: basename(fileName),
|
|
91
|
+
exists: false,
|
|
92
|
+
searchedFiles,
|
|
93
|
+
...options.configPattern ? { configPattern: options.configPattern } : {}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function formatSearchedFiles(files, pattern) {
|
|
97
|
+
if (files.length > 0) return files.map((file) => `\`${file}\``).join(", ");
|
|
98
|
+
if (pattern) return `files matching ${pattern.toString()}`;
|
|
99
|
+
return `\`${LOCALGHOST_CONFIG_FILE}\``;
|
|
100
|
+
}
|
|
101
|
+
function readDevHosts(options = {}) {
|
|
102
|
+
const resolvedOptions = typeof options === "string" ? { cwd: options } : options;
|
|
103
|
+
const resolvedPath = resolveDevHostsPath(resolvedOptions);
|
|
104
|
+
if (!resolvedPath.exists) {
|
|
105
|
+
const cwd = resolvedOptions.cwd ?? process.cwd();
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Missing Localghost config in ${cwd}. Looked for ${formatSearchedFiles(resolvedPath.searchedFiles, resolvedPath.configPattern)}. Run \`localghost init\` or pass --config/--config-pattern.`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return parseDevHosts(readFileSync(resolvedPath.path, "utf8"), resolvedPath.fileName);
|
|
111
|
+
}
|
|
112
|
+
function getProjectName(cwd = process.cwd()) {
|
|
113
|
+
try {
|
|
114
|
+
const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
|
|
115
|
+
const name = typeof pkg.name === "string" && pkg.name ? pkg.name : "app";
|
|
116
|
+
return sanitizeProjectName(name.replace(/^@/, ""));
|
|
117
|
+
} catch {
|
|
118
|
+
return "app";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function sanitizeProjectName(value) {
|
|
122
|
+
const projectName = value.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
123
|
+
return projectName || "app";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/caddy.ts
|
|
127
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
128
|
+
import { execa } from "execa";
|
|
129
|
+
|
|
130
|
+
// src/fs.ts
|
|
131
|
+
import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
132
|
+
import { dirname } from "path";
|
|
133
|
+
function readTextFile(path) {
|
|
134
|
+
return readFileSync2(path, "utf8");
|
|
135
|
+
}
|
|
136
|
+
function writeTextFile(path, value) {
|
|
137
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
138
|
+
writeFileSync(path, value, "utf8");
|
|
139
|
+
return path;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/caddy.ts
|
|
143
|
+
function groupByPort(entries) {
|
|
144
|
+
const groups = /* @__PURE__ */ new Map();
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
const group = groups.get(entry.port) ?? [];
|
|
147
|
+
group.push(entry);
|
|
148
|
+
groups.set(entry.port, group);
|
|
149
|
+
}
|
|
150
|
+
return groups;
|
|
151
|
+
}
|
|
152
|
+
function getCaddyfilePath(cwd = process.cwd()) {
|
|
153
|
+
return join2(cwd, "ops/local/Caddyfile");
|
|
154
|
+
}
|
|
155
|
+
function renderCaddyfile(entries) {
|
|
156
|
+
const groups = groupByPort(entries);
|
|
157
|
+
const blocks = [...groups.entries()].sort(([leftPort], [rightPort]) => leftPort - rightPort).map(([port, group]) => {
|
|
158
|
+
const hosts = group.map((entry) => entry.host).sort().join(", ");
|
|
159
|
+
return `${hosts} {
|
|
160
|
+
reverse_proxy 127.0.0.1:${port}
|
|
161
|
+
}`;
|
|
162
|
+
});
|
|
163
|
+
return `{
|
|
164
|
+
local_certs
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
${blocks.join("\n\n")}
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
async function writeCaddyfile(entries, cwd = process.cwd()) {
|
|
171
|
+
const path = getCaddyfilePath(cwd);
|
|
172
|
+
writeTextFile(path, renderCaddyfile(entries));
|
|
173
|
+
return path;
|
|
174
|
+
}
|
|
175
|
+
async function validateCaddyfile(path) {
|
|
176
|
+
await execa("caddy", ["validate", "--config", path], {
|
|
177
|
+
cwd: dirname2(path),
|
|
178
|
+
stdio: "inherit"
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
async function runCaddy(path) {
|
|
182
|
+
await execa("caddy", ["run", "--config", path], {
|
|
183
|
+
cwd: dirname2(path),
|
|
184
|
+
stdio: "inherit"
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/doctor.ts
|
|
189
|
+
import { execa as execa2 } from "execa";
|
|
190
|
+
async function checkCaddy() {
|
|
191
|
+
try {
|
|
192
|
+
const result = await execa2("caddy", ["version"], { reject: false });
|
|
193
|
+
const version = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
194
|
+
return {
|
|
195
|
+
found: result.exitCode === 0,
|
|
196
|
+
...version ? { version } : {},
|
|
197
|
+
installHint: "brew install caddy"
|
|
198
|
+
};
|
|
199
|
+
} catch {
|
|
200
|
+
return {
|
|
201
|
+
found: false,
|
|
202
|
+
installHint: "brew install caddy"
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async function runDoctor() {
|
|
207
|
+
const caddy = await checkCaddy();
|
|
208
|
+
return {
|
|
209
|
+
ok: caddy.found,
|
|
210
|
+
caddy
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/hosts-file.ts
|
|
215
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
216
|
+
import { tmpdir } from "os";
|
|
217
|
+
import { join as join3 } from "path";
|
|
218
|
+
import { execa as execa3 } from "execa";
|
|
219
|
+
function escapeRegExp(value) {
|
|
220
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
221
|
+
}
|
|
222
|
+
function getManagedBlockPattern(projectName) {
|
|
223
|
+
const sanitizedProjectName = sanitizeProjectName(projectName);
|
|
224
|
+
const start = `# localghost:start ${sanitizedProjectName}`;
|
|
225
|
+
const end = `# localghost:end ${sanitizedProjectName}`;
|
|
226
|
+
return new RegExp(`${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}\\n?`, "m");
|
|
227
|
+
}
|
|
228
|
+
function getSystemHostsPath() {
|
|
229
|
+
return process.platform === "win32" ? "C:\\Windows\\System32\\drivers\\etc\\hosts" : "/etc/hosts";
|
|
230
|
+
}
|
|
231
|
+
function renderHostsBlock(projectName, entries) {
|
|
232
|
+
const sanitizedProjectName = sanitizeProjectName(projectName);
|
|
233
|
+
const hosts = [...new Set(entries.map((entry) => entry.host))].sort();
|
|
234
|
+
return [
|
|
235
|
+
`# localghost:start ${sanitizedProjectName}`,
|
|
236
|
+
...hosts.map((host) => `127.0.0.1 ${host}`),
|
|
237
|
+
`# localghost:end ${sanitizedProjectName}`,
|
|
238
|
+
""
|
|
239
|
+
].join("\n");
|
|
240
|
+
}
|
|
241
|
+
function upsertManagedBlock(existing, projectName, block) {
|
|
242
|
+
const pattern = getManagedBlockPattern(projectName);
|
|
243
|
+
if (pattern.test(existing)) {
|
|
244
|
+
return existing.replace(pattern, block);
|
|
245
|
+
}
|
|
246
|
+
return `${existing.trimEnd()}
|
|
247
|
+
|
|
248
|
+
${block}`;
|
|
249
|
+
}
|
|
250
|
+
function removeManagedBlock(existing, projectName) {
|
|
251
|
+
const pattern = getManagedBlockPattern(projectName);
|
|
252
|
+
if (!pattern.test(existing)) {
|
|
253
|
+
return existing;
|
|
254
|
+
}
|
|
255
|
+
return existing.replace(pattern, "").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
256
|
+
}
|
|
257
|
+
async function writeSystemHostsFile(hostsPath, next, projectName) {
|
|
258
|
+
const sanitizedProjectName = sanitizeProjectName(projectName);
|
|
259
|
+
const tempPath = join3(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
|
|
260
|
+
writeFileSync2(tempPath, next, "utf8");
|
|
261
|
+
if (process.platform === "win32") {
|
|
262
|
+
throw new Error(`Windows support: run as administrator and copy ${tempPath} to ${hostsPath}.`);
|
|
263
|
+
}
|
|
264
|
+
await execa3("sudo", ["cp", tempPath, hostsPath], { stdio: "inherit" });
|
|
265
|
+
return tempPath;
|
|
266
|
+
}
|
|
267
|
+
async function updateSystemHosts(projectName, entries) {
|
|
268
|
+
const sanitizedProjectName = sanitizeProjectName(projectName);
|
|
269
|
+
const hostsPath = getSystemHostsPath();
|
|
270
|
+
const existing = readTextFile(hostsPath);
|
|
271
|
+
const block = renderHostsBlock(sanitizedProjectName, entries);
|
|
272
|
+
const next = upsertManagedBlock(existing, sanitizedProjectName, block);
|
|
273
|
+
if (next === existing) {
|
|
274
|
+
return { changed: false, hostsPath };
|
|
275
|
+
}
|
|
276
|
+
const tempPath = await writeSystemHostsFile(hostsPath, next, sanitizedProjectName);
|
|
277
|
+
return { changed: true, hostsPath, tempPath };
|
|
278
|
+
}
|
|
279
|
+
async function removeSystemHosts(projectName) {
|
|
280
|
+
const sanitizedProjectName = sanitizeProjectName(projectName);
|
|
281
|
+
const hostsPath = getSystemHostsPath();
|
|
282
|
+
const existing = readTextFile(hostsPath);
|
|
283
|
+
const next = removeManagedBlock(existing, sanitizedProjectName);
|
|
284
|
+
if (next === existing) {
|
|
285
|
+
return { changed: false, removed: false, hostsPath };
|
|
286
|
+
}
|
|
287
|
+
const tempPath = await writeSystemHostsFile(hostsPath, next, sanitizedProjectName);
|
|
288
|
+
return { changed: true, removed: true, hostsPath, tempPath };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/init.ts
|
|
292
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
293
|
+
import { join as join4 } from "path";
|
|
294
|
+
function detectPackageManager(cwd = process.cwd()) {
|
|
295
|
+
if (existsSync2(join4(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
296
|
+
if (existsSync2(join4(cwd, "yarn.lock"))) return "yarn";
|
|
297
|
+
return "npm";
|
|
298
|
+
}
|
|
299
|
+
function packageRunCommand(packageManager, script) {
|
|
300
|
+
if (packageManager === "yarn") return `yarn ${script}`;
|
|
301
|
+
if (packageManager === "pnpm") return `pnpm ${script}`;
|
|
302
|
+
return `npm run ${script}`;
|
|
303
|
+
}
|
|
304
|
+
function renderConfig(options) {
|
|
305
|
+
return [
|
|
306
|
+
"# Buh. Friendly names for local services.",
|
|
307
|
+
"# Format: <host> <port>",
|
|
308
|
+
`${options.host} ${options.port}`,
|
|
309
|
+
`www.${options.host} ${options.port}`,
|
|
310
|
+
`${options.apiHost} ${options.apiPort}`,
|
|
311
|
+
""
|
|
312
|
+
].join("\n");
|
|
313
|
+
}
|
|
314
|
+
function readPackageJson(path) {
|
|
315
|
+
try {
|
|
316
|
+
return JSON.parse(readFileSync3(path, "utf8"));
|
|
317
|
+
} catch {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function shellQuote(value) {
|
|
322
|
+
if (/^[A-Za-z0-9_./:-]+$/.test(value)) return value;
|
|
323
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
324
|
+
}
|
|
325
|
+
function getConfigFlag(configFile) {
|
|
326
|
+
return configFile === LOCALGHOST_CONFIG_FILE ? "" : ` --config ${shellQuote(configFile)}`;
|
|
327
|
+
}
|
|
328
|
+
function updatePackageScripts(packageJsonPath, configFile) {
|
|
329
|
+
const pkg = readPackageJson(packageJsonPath);
|
|
330
|
+
if (!pkg) return false;
|
|
331
|
+
const scripts = typeof pkg.scripts === "object" && pkg.scripts ? pkg.scripts : {};
|
|
332
|
+
const configFlag = getConfigFlag(configFile);
|
|
333
|
+
const nextScripts = {
|
|
334
|
+
...scripts,
|
|
335
|
+
"localghost:setup": scripts["localghost:setup"] ?? `localghost setup${configFlag}`,
|
|
336
|
+
"localghost:proxy": scripts["localghost:proxy"] ?? `localghost dev${configFlag}`,
|
|
337
|
+
"localghost:print": scripts["localghost:print"] ?? `localghost print${configFlag}`,
|
|
338
|
+
"localghost:routes": scripts["localghost:routes"] ?? `localghost routes${configFlag}`,
|
|
339
|
+
"localghost:status": scripts["localghost:status"] ?? "localghost status",
|
|
340
|
+
"localghost:teardown": scripts["localghost:teardown"] ?? "localghost teardown",
|
|
341
|
+
"localghost:doctor": scripts["localghost:doctor"] ?? "localghost doctor",
|
|
342
|
+
"localghost:update": scripts["localghost:update"] ?? "localghost update"
|
|
343
|
+
};
|
|
344
|
+
const changed = JSON.stringify(scripts) !== JSON.stringify(nextScripts);
|
|
345
|
+
if (!changed) return false;
|
|
346
|
+
pkg.scripts = nextScripts;
|
|
347
|
+
writeFileSync3(packageJsonPath, `${JSON.stringify(pkg, null, 2)}
|
|
348
|
+
`, "utf8");
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
function initLocalghost(options = {}) {
|
|
352
|
+
const cwd = options.cwd ?? process.cwd();
|
|
353
|
+
const projectName = sanitizeProjectName(getProjectName(cwd).split("/").pop() ?? "app");
|
|
354
|
+
const host = options.host ?? `${projectName}.localhost`;
|
|
355
|
+
const port = options.port ?? 5173;
|
|
356
|
+
const apiHost = options.apiHost ?? `api.${host}`;
|
|
357
|
+
const apiPort = options.apiPort ?? 8787;
|
|
358
|
+
const packageManager = options.packageManager ?? detectPackageManager(cwd);
|
|
359
|
+
const configFile = options.configFile ?? LOCALGHOST_CONFIG_FILE;
|
|
360
|
+
const configPath = join4(cwd, configFile);
|
|
361
|
+
const configExists = existsSync2(configPath);
|
|
362
|
+
if (configExists && !options.force) {
|
|
363
|
+
return {
|
|
364
|
+
configPath,
|
|
365
|
+
configCreated: false,
|
|
366
|
+
packageJsonChanged: false,
|
|
367
|
+
packageManager,
|
|
368
|
+
nextSteps: [
|
|
369
|
+
packageRunCommand(packageManager, "localghost:doctor"),
|
|
370
|
+
packageRunCommand(packageManager, "localghost:setup"),
|
|
371
|
+
packageRunCommand(packageManager, "localghost:proxy")
|
|
372
|
+
]
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
writeTextFile(configPath, renderConfig({ host, port, apiHost, apiPort }));
|
|
376
|
+
const packageJsonPath = join4(cwd, "package.json");
|
|
377
|
+
const packageJsonChanged = options.writeScripts ? updatePackageScripts(packageJsonPath, configFile) : false;
|
|
378
|
+
return {
|
|
379
|
+
configPath,
|
|
380
|
+
configCreated: true,
|
|
381
|
+
...existsSync2(packageJsonPath) ? { packageJsonPath } : {},
|
|
382
|
+
packageJsonChanged,
|
|
383
|
+
packageManager,
|
|
384
|
+
nextSteps: [
|
|
385
|
+
packageRunCommand(packageManager, "localghost:doctor"),
|
|
386
|
+
packageRunCommand(packageManager, "localghost:setup"),
|
|
387
|
+
packageRunCommand(packageManager, "localghost:proxy")
|
|
388
|
+
]
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// src/routes.ts
|
|
393
|
+
function getDomainRoutes(entries, options = {}) {
|
|
394
|
+
const protocol = options.https === false ? "http" : "https";
|
|
395
|
+
return [...entries].sort((left, right) => left.host.localeCompare(right.host) || left.port - right.port).map((entry) => ({
|
|
396
|
+
host: entry.host,
|
|
397
|
+
port: entry.port,
|
|
398
|
+
url: `${protocol}://${entry.host}/`,
|
|
399
|
+
upstream: `http://${entry.target}`
|
|
400
|
+
}));
|
|
401
|
+
}
|
|
402
|
+
function formatDomainRoutes(entries, options = {}) {
|
|
403
|
+
const routes = getDomainRoutes(entries, options);
|
|
404
|
+
if (routes.length === 0) {
|
|
405
|
+
return "localghost routes\n no routes";
|
|
406
|
+
}
|
|
407
|
+
return [
|
|
408
|
+
"localghost routes",
|
|
409
|
+
...routes.map((route) => ` ${route.url} -> ${route.upstream}`)
|
|
410
|
+
].join("\n");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// src/state.ts
|
|
414
|
+
import { existsSync as existsSync3 } from "fs";
|
|
415
|
+
import { join as join5 } from "path";
|
|
416
|
+
var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
|
|
417
|
+
function getLocalghostStatePath(cwd = process.cwd()) {
|
|
418
|
+
return join5(cwd, LOCALGHOST_STATE_FILE);
|
|
419
|
+
}
|
|
420
|
+
function readLocalghostState(cwd = process.cwd()) {
|
|
421
|
+
const path = getLocalghostStatePath(cwd);
|
|
422
|
+
if (!existsSync3(path)) return null;
|
|
423
|
+
return JSON.parse(readTextFile(path));
|
|
424
|
+
}
|
|
425
|
+
function writeLocalghostState(cwd, state) {
|
|
426
|
+
const path = getLocalghostStatePath(cwd);
|
|
427
|
+
writeTextFile(path, `${JSON.stringify({ version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), ...state }, null, 2)}
|
|
428
|
+
`);
|
|
429
|
+
return path;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// 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";
|
|
436
|
+
var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
|
|
437
|
+
var LOCALGHOST_VERSION = "0.1.0";
|
|
438
|
+
var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
439
|
+
var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
440
|
+
var UPDATE_CHECK_TIMEOUT_MS = 900;
|
|
441
|
+
function truthyEnv(value) {
|
|
442
|
+
return value === "1" || value === "true" || value === "yes";
|
|
443
|
+
}
|
|
444
|
+
function isUpdateCheckDisabled(env = process.env) {
|
|
445
|
+
return truthyEnv(env.LOCALGHOST_NO_UPDATE_CHECK);
|
|
446
|
+
}
|
|
447
|
+
function getUpdateCheckCachePath(env = process.env) {
|
|
448
|
+
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");
|
|
451
|
+
}
|
|
452
|
+
function readCache(path = getUpdateCheckCachePath()) {
|
|
453
|
+
if (!existsSync4(path)) return null;
|
|
454
|
+
try {
|
|
455
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
456
|
+
} catch {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function writeCache(cache, path = getUpdateCheckCachePath()) {
|
|
461
|
+
try {
|
|
462
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
463
|
+
writeFileSync4(path, `${JSON.stringify(cache, null, 2)}
|
|
464
|
+
`, "utf8");
|
|
465
|
+
} catch {
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function ageMs(date, now = Date.now()) {
|
|
469
|
+
if (!date) return Number.POSITIVE_INFINITY;
|
|
470
|
+
const time = Date.parse(date);
|
|
471
|
+
return Number.isFinite(time) ? now - time : Number.POSITIVE_INFINITY;
|
|
472
|
+
}
|
|
473
|
+
function isCacheFresh(cache, ttlMs, now = Date.now()) {
|
|
474
|
+
return Boolean(cache?.latestVersion && ageMs(cache.checkedAt, now) >= 0 && ageMs(cache.checkedAt, now) < ttlMs);
|
|
475
|
+
}
|
|
476
|
+
function parseVersion(version) {
|
|
477
|
+
const match = version.trim().replace(/^v/, "").match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
|
|
478
|
+
if (!match) return null;
|
|
479
|
+
return {
|
|
480
|
+
major: Number(match[1]),
|
|
481
|
+
minor: Number(match[2]),
|
|
482
|
+
patch: Number(match[3]),
|
|
483
|
+
...match[4] ? { prerelease: match[4] } : {}
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
function compareVersions(a, b) {
|
|
487
|
+
const left = parseVersion(a);
|
|
488
|
+
const right = parseVersion(b);
|
|
489
|
+
if (!left || !right) return a.localeCompare(b);
|
|
490
|
+
for (const key of ["major", "minor", "patch"]) {
|
|
491
|
+
if (left[key] !== right[key]) return left[key] > right[key] ? 1 : -1;
|
|
492
|
+
}
|
|
493
|
+
if (left.prerelease === right.prerelease) return 0;
|
|
494
|
+
if (!left.prerelease) return 1;
|
|
495
|
+
if (!right.prerelease) return -1;
|
|
496
|
+
return left.prerelease.localeCompare(right.prerelease);
|
|
497
|
+
}
|
|
498
|
+
function isNewerVersion(candidate, current = LOCALGHOST_VERSION) {
|
|
499
|
+
return Boolean(candidate && compareVersions(candidate, current) > 0);
|
|
500
|
+
}
|
|
501
|
+
async function fetchLatestVersion(packageName, timeoutMs) {
|
|
502
|
+
const encodedName = packageName.startsWith("@") ? `@${packageName.slice(1).replace("/", "%2f")}` : packageName;
|
|
503
|
+
const response = await fetch(`https://registry.npmjs.org/${encodedName}`, {
|
|
504
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
505
|
+
headers: {
|
|
506
|
+
accept: "application/vnd.npm.install-v1+json"
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
if (!response.ok) throw new Error(`npm registry returned ${response.status}`);
|
|
510
|
+
const data = await response.json();
|
|
511
|
+
const latest = data["dist-tags"]?.latest;
|
|
512
|
+
if (typeof latest !== "string" || latest.length === 0) throw new Error("npm registry response did not include latest dist-tag");
|
|
513
|
+
return latest;
|
|
514
|
+
}
|
|
515
|
+
async function checkForUpdate(options = {}) {
|
|
516
|
+
const env = options.env ?? process.env;
|
|
517
|
+
const packageName = options.packageName ?? LOCALGHOST_PACKAGE_NAME;
|
|
518
|
+
const currentVersion = options.currentVersion ?? LOCALGHOST_VERSION;
|
|
519
|
+
const cachePath = options.cachePath ?? getUpdateCheckCachePath(env);
|
|
520
|
+
if (!options.force && isUpdateCheckDisabled(env)) {
|
|
521
|
+
return {
|
|
522
|
+
currentVersion,
|
|
523
|
+
packageName,
|
|
524
|
+
updateAvailable: false,
|
|
525
|
+
source: "disabled"
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
const cache = readCache(cachePath);
|
|
529
|
+
if (!options.force && isCacheFresh(cache, UPDATE_CHECK_CACHE_TTL_MS)) {
|
|
530
|
+
const latestVersion = cache?.latestVersion;
|
|
531
|
+
return {
|
|
532
|
+
currentVersion,
|
|
533
|
+
packageName,
|
|
534
|
+
...latestVersion ? { latestVersion } : {},
|
|
535
|
+
updateAvailable: isNewerVersion(latestVersion, currentVersion),
|
|
536
|
+
source: "cache"
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
try {
|
|
540
|
+
const latestVersion = await fetchLatestVersion(packageName, options.timeoutMs ?? UPDATE_CHECK_TIMEOUT_MS);
|
|
541
|
+
writeCache({ checkedAt: (/* @__PURE__ */ new Date()).toISOString(), latestVersion }, cachePath);
|
|
542
|
+
return {
|
|
543
|
+
currentVersion,
|
|
544
|
+
packageName,
|
|
545
|
+
latestVersion,
|
|
546
|
+
updateAvailable: isNewerVersion(latestVersion, currentVersion),
|
|
547
|
+
source: "registry"
|
|
548
|
+
};
|
|
549
|
+
} catch (error) {
|
|
550
|
+
const latestVersion = cache?.latestVersion;
|
|
551
|
+
return {
|
|
552
|
+
currentVersion,
|
|
553
|
+
packageName,
|
|
554
|
+
...latestVersion ? { latestVersion } : {},
|
|
555
|
+
updateAvailable: isNewerVersion(latestVersion, currentVersion),
|
|
556
|
+
source: "error",
|
|
557
|
+
error: error instanceof Error ? error.message : String(error)
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function formatUpdateMessage(result) {
|
|
562
|
+
if (!result.updateAvailable || !result.latestVersion) return null;
|
|
563
|
+
return [
|
|
564
|
+
`localghost ${result.latestVersion} is available. Current: ${result.currentVersion}`,
|
|
565
|
+
`Update with: npm i -g ${result.packageName}@latest`
|
|
566
|
+
].join("\n");
|
|
567
|
+
}
|
|
568
|
+
function shouldNotifyAboutUpdate(result, cachePath = getUpdateCheckCachePath(), now = Date.now()) {
|
|
569
|
+
if (!result.updateAvailable || !result.latestVersion) return false;
|
|
570
|
+
const cache = readCache(cachePath);
|
|
571
|
+
if (cache?.notifiedVersion !== result.latestVersion) return true;
|
|
572
|
+
return ageMs(cache.notifiedAt, now) >= UPDATE_CHECK_NOTIFY_TTL_MS;
|
|
573
|
+
}
|
|
574
|
+
function markUpdateNotified(result, cachePath = getUpdateCheckCachePath()) {
|
|
575
|
+
if (!result.latestVersion) return;
|
|
576
|
+
const cache = readCache(cachePath) ?? { checkedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
577
|
+
writeCache(
|
|
578
|
+
{
|
|
579
|
+
...cache,
|
|
580
|
+
latestVersion: result.latestVersion,
|
|
581
|
+
notifiedVersion: result.latestVersion,
|
|
582
|
+
notifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
583
|
+
},
|
|
584
|
+
cachePath
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
async function maybeNotifyAboutUpdate(options = {}) {
|
|
588
|
+
if (options.disabled) return;
|
|
589
|
+
const cachePath = getUpdateCheckCachePath();
|
|
590
|
+
const result = await checkForUpdate({ cachePath });
|
|
591
|
+
if (!shouldNotifyAboutUpdate(result, cachePath)) return;
|
|
592
|
+
const message = formatUpdateMessage(result);
|
|
593
|
+
if (!message) return;
|
|
594
|
+
console.warn(`
|
|
595
|
+
${message}`);
|
|
596
|
+
markUpdateNotified(result, cachePath);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// src/cli.ts
|
|
600
|
+
function warnAboutLocalMdns(entries) {
|
|
601
|
+
const localHosts = findLocalMdnsHosts(entries);
|
|
602
|
+
if (localHosts.length > 0) {
|
|
603
|
+
console.warn(
|
|
604
|
+
`Warning: .local can collide with mDNS/Bonjour. Prefer .localhost for dev hosts: ${localHosts.join(", ")}`
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function logDomainRoutes(entries) {
|
|
609
|
+
console.log(formatDomainRoutes(entries));
|
|
610
|
+
}
|
|
611
|
+
function parsePort(value) {
|
|
612
|
+
const port = Number.parseInt(value, 10);
|
|
613
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
614
|
+
throw new InvalidArgumentError("Port must be a number between 1 and 65535.");
|
|
615
|
+
}
|
|
616
|
+
return port;
|
|
617
|
+
}
|
|
618
|
+
function parsePackageManager(value) {
|
|
619
|
+
if (value === "npm" || value === "yarn" || value === "pnpm") return value;
|
|
620
|
+
throw new InvalidArgumentError("Package manager must be npm, yarn, or pnpm.");
|
|
621
|
+
}
|
|
622
|
+
function collect(value, previous = []) {
|
|
623
|
+
return [...previous, value];
|
|
624
|
+
}
|
|
625
|
+
function readOptionsFromCli(options) {
|
|
626
|
+
return {
|
|
627
|
+
cwd: options.cwd,
|
|
628
|
+
...options.config && options.config.length > 0 ? { configFiles: options.config } : {},
|
|
629
|
+
...options.configPattern ? { configPattern: options.configPattern } : {}
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
async function assertCaddyReady() {
|
|
633
|
+
const caddy = await checkCaddy();
|
|
634
|
+
if (caddy.found) return;
|
|
635
|
+
throw new Error([
|
|
636
|
+
"Caddy was not found.",
|
|
637
|
+
`Install it with: ${caddy.installHint}`,
|
|
638
|
+
"Localghost will not install it for you. No surprise spells."
|
|
639
|
+
].join("\n"));
|
|
640
|
+
}
|
|
641
|
+
var program = new Command();
|
|
642
|
+
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
|
+
program.hook("postAction", async (_thisCommand, actionCommand) => {
|
|
644
|
+
if (actionCommand.name() === "update") return;
|
|
645
|
+
const options = program.opts();
|
|
646
|
+
await maybeNotifyAboutUpdate({ disabled: options.updateCheck === false });
|
|
647
|
+
});
|
|
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) => {
|
|
649
|
+
const result = initLocalghost({ ...options, configFile: options.config });
|
|
650
|
+
if (result.configCreated) {
|
|
651
|
+
console.log(`Buh. Created ${result.configPath}`);
|
|
652
|
+
} else {
|
|
653
|
+
console.log(`${result.configPath} already exists. Use --force to rewrite it.`);
|
|
654
|
+
}
|
|
655
|
+
if (options.writeScripts) {
|
|
656
|
+
if (result.packageJsonChanged) {
|
|
657
|
+
console.log(`Updated ${result.packageJsonPath}`);
|
|
658
|
+
} else if (result.packageJsonPath) {
|
|
659
|
+
console.log(`${result.packageJsonPath} already has localghost scripts.`);
|
|
660
|
+
} else {
|
|
661
|
+
console.log("No package.json found; skipped script setup.");
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
console.log("Next:");
|
|
665
|
+
for (const step of result.nextSteps) {
|
|
666
|
+
console.log(` ${step}`);
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
program.command("doctor").description("Check machine prerequisites").action(async () => {
|
|
670
|
+
const result = await runDoctor();
|
|
671
|
+
if (result.caddy.found) {
|
|
672
|
+
console.log(`Caddy: ${result.caddy.version ?? "found"}`);
|
|
673
|
+
} else {
|
|
674
|
+
console.log("Caddy: missing");
|
|
675
|
+
console.log(`Run: ${result.caddy.installHint}`);
|
|
676
|
+
console.log("Localghost will not install it for you. No surprise spells.");
|
|
677
|
+
}
|
|
678
|
+
if (!result.ok) {
|
|
679
|
+
process.exitCode = 1;
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
program.command("update").description("Check npm for a newer localghost release").option("--json", "Print raw JSON").action(async (options) => {
|
|
683
|
+
const result = await checkForUpdate({ force: true, timeoutMs: 5e3 });
|
|
684
|
+
if (options.json) {
|
|
685
|
+
console.log(JSON.stringify(result, null, 2));
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const message = formatUpdateMessage(result);
|
|
689
|
+
if (message) {
|
|
690
|
+
console.log(message);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (result.source === "error") {
|
|
694
|
+
console.log(`Could not check npm for updates: ${result.error ?? "unknown error"}`);
|
|
695
|
+
process.exitCode = 1;
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
console.log(`localghost is up to date. Current: ${result.currentVersion}`);
|
|
699
|
+
});
|
|
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) => {
|
|
701
|
+
await assertCaddyReady();
|
|
702
|
+
const projectName = sanitizeProjectName(options.project ?? getProjectName(options.cwd));
|
|
703
|
+
const readOptions = readOptionsFromCli(options);
|
|
704
|
+
const configPath = resolveDevHostsPath(readOptions).path;
|
|
705
|
+
const entries = readDevHosts(readOptions);
|
|
706
|
+
warnAboutLocalMdns(entries);
|
|
707
|
+
logDomainRoutes(entries);
|
|
708
|
+
const hostsResult = await updateSystemHosts(projectName, entries);
|
|
709
|
+
if (hostsResult.changed) {
|
|
710
|
+
console.log(`Updated ${hostsResult.hostsPath}`);
|
|
711
|
+
} else {
|
|
712
|
+
console.log(`${hostsResult.hostsPath} already up to date`);
|
|
713
|
+
}
|
|
714
|
+
const caddyfile = await writeCaddyfile(entries, options.cwd);
|
|
715
|
+
await validateCaddyfile(caddyfile);
|
|
716
|
+
const statePath = writeLocalghostState(options.cwd, {
|
|
717
|
+
action: "setup",
|
|
718
|
+
projectName,
|
|
719
|
+
cwd: options.cwd,
|
|
720
|
+
configPath,
|
|
721
|
+
hostsPath: hostsResult.hostsPath,
|
|
722
|
+
hostsChanged: hostsResult.changed,
|
|
723
|
+
...hostsResult.tempPath ? { hostsTempPath: hostsResult.tempPath } : {},
|
|
724
|
+
caddyfilePath: caddyfile,
|
|
725
|
+
entries
|
|
726
|
+
});
|
|
727
|
+
console.log(`Generated ${caddyfile}`);
|
|
728
|
+
console.log(`State ${statePath}`);
|
|
729
|
+
console.log("Setup complete.");
|
|
730
|
+
});
|
|
731
|
+
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) => {
|
|
732
|
+
const projectName = sanitizeProjectName(options.project ?? getProjectName(options.cwd));
|
|
733
|
+
const hostsResult = await removeSystemHosts(projectName);
|
|
734
|
+
const caddyfilePath = getCaddyfilePath(options.cwd);
|
|
735
|
+
let caddyfileRemoved = false;
|
|
736
|
+
if (options.removeCaddyfile && existsSync5(caddyfilePath)) {
|
|
737
|
+
unlinkSync(caddyfilePath);
|
|
738
|
+
caddyfileRemoved = true;
|
|
739
|
+
}
|
|
740
|
+
const statePath = writeLocalghostState(options.cwd, {
|
|
741
|
+
action: "teardown",
|
|
742
|
+
projectName,
|
|
743
|
+
cwd: options.cwd,
|
|
744
|
+
hostsPath: hostsResult.hostsPath,
|
|
745
|
+
hostsChanged: hostsResult.changed,
|
|
746
|
+
...hostsResult.tempPath ? { hostsTempPath: hostsResult.tempPath } : {},
|
|
747
|
+
caddyfilePath,
|
|
748
|
+
caddyfileRemoved
|
|
749
|
+
});
|
|
750
|
+
if (hostsResult.removed) {
|
|
751
|
+
console.log(`Removed Localghost hosts block from ${hostsResult.hostsPath}`);
|
|
752
|
+
} else {
|
|
753
|
+
console.log(`No Localghost hosts block found in ${hostsResult.hostsPath}`);
|
|
754
|
+
}
|
|
755
|
+
if (options.removeCaddyfile) {
|
|
756
|
+
console.log(caddyfileRemoved ? `Removed ${caddyfilePath}` : `${caddyfilePath} was not present`);
|
|
757
|
+
}
|
|
758
|
+
console.log(`State ${statePath}`);
|
|
759
|
+
});
|
|
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) => {
|
|
761
|
+
const state = readLocalghostState(options.cwd);
|
|
762
|
+
const statePath = getLocalghostStatePath(options.cwd);
|
|
763
|
+
if (!state) {
|
|
764
|
+
console.log(`No Localghost state found at ${statePath}`);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (options.json) {
|
|
768
|
+
console.log(JSON.stringify(state, null, 2));
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
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}`);
|
|
779
|
+
});
|
|
780
|
+
program.command("routes").description("Print domain to upstream routes").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to look for. Can be repeated.", collect, []).option("--config-pattern <regex>", "Regex for config filenames in the project root").option("--http", "Print domain URLs with http instead of https").action((options) => {
|
|
781
|
+
const entries = readDevHosts(readOptionsFromCli(options));
|
|
782
|
+
warnAboutLocalMdns(entries);
|
|
783
|
+
console.log(formatDomainRoutes(entries, { https: !options.http }));
|
|
784
|
+
});
|
|
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) => {
|
|
786
|
+
await assertCaddyReady();
|
|
787
|
+
const entries = readDevHosts(readOptionsFromCli(options));
|
|
788
|
+
warnAboutLocalMdns(entries);
|
|
789
|
+
logDomainRoutes(entries);
|
|
790
|
+
const caddyfile = await writeCaddyfile(entries, options.cwd);
|
|
791
|
+
await validateCaddyfile(caddyfile);
|
|
792
|
+
await runCaddy(caddyfile);
|
|
793
|
+
});
|
|
794
|
+
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
|
+
const entries = readDevHosts(readOptionsFromCli(options));
|
|
796
|
+
warnAboutLocalMdns(entries);
|
|
797
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
798
|
+
});
|
|
799
|
+
program.parseAsync().catch((error) => {
|
|
800
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
801
|
+
console.error(message);
|
|
802
|
+
process.exitCode = 1;
|
|
803
|
+
});
|
|
804
|
+
//# sourceMappingURL=cli.js.map
|