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