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