@hamedb89/localghost 0.1.8 → 0.1.10
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 +56 -327
- package/apps/macos-widget/LocalghostWidget.entitlements +12 -0
- package/apps/macos-widget/LocalghostWidget.swift +701 -48
- package/apps/macos-widget/Resources/localghost-logo-source.png +0 -0
- package/apps/macos-widget/Resources/localghost-widget-ui-reference.png +0 -0
- package/apps/macos-widget/Shared/LocalghostWidgetSnapshot.swift +50 -0
- package/apps/macos-widget/WidgetExtension/LocalghostDesktopWidget.swift +176 -0
- package/apps/macos-widget/WidgetExtension/LocalghostDesktopWidgetExtension.entitlements +12 -0
- package/apps/macos-widget/build.sh +8 -1
- package/apps/macos-widget/project.yml +40 -0
- package/assets/ghost-tunnel-app-icon.png +0 -0
- package/assets/ghost-tunnel-portal.png +0 -0
- package/assets/ghost-tunnel-wordmark.png +0 -0
- package/dist/cli.js +528 -63
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +130 -5
- package/dist/index.js +765 -31
- package/dist/index.js.map +1 -1
- package/dist/tunnel-DzfLXZ8O.d.ts +126 -0
- package/dist/vite.d.ts +3 -1
- package/dist/vite.js +660 -36
- package/dist/vite.js.map +1 -1
- package/docs/flows.md +19 -0
- package/docs/ghost-tunnel.md +293 -0
- package/docs/github.md +6 -6
- package/docs/localghost.1.md +10 -4
- package/docs/macos-widget.md +68 -6
- package/package.json +6 -2
- package/dist/config-Cde1Bich.d.ts +0 -31
package/dist/vite.js
CHANGED
|
@@ -1,10 +1,125 @@
|
|
|
1
1
|
// src/vite.ts
|
|
2
|
-
import { existsSync as
|
|
2
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
3
3
|
import { normalize, resolve as resolve2 } from "path";
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
import { emitKeypressEvents } from "readline";
|
|
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: [], setups: [] };
|
|
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
|
+
setups: Array.isArray(parsed.setups) ? parsed.setups : []
|
|
38
|
+
};
|
|
39
|
+
} catch {
|
|
40
|
+
return emptyActivity();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function writeLocalghostActivity(activity, path = getLocalghostActivityPath()) {
|
|
44
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
45
|
+
writeFileSync(path, `${JSON.stringify(activity, null, 2)}
|
|
46
|
+
`, "utf8");
|
|
47
|
+
return path;
|
|
48
|
+
}
|
|
49
|
+
function createRunId(input2, pid) {
|
|
50
|
+
return `${input2.projectName}:${input2.mode}:${pid}:${Date.now()}`;
|
|
51
|
+
}
|
|
52
|
+
function createSetupId(input2) {
|
|
53
|
+
return `${input2.projectName}:${input2.cwd}:${input2.configPath ?? ""}`;
|
|
54
|
+
}
|
|
55
|
+
function pruneLocalghostActivity(path = getLocalghostActivityPath()) {
|
|
56
|
+
const activity = readLocalghostActivity(path);
|
|
57
|
+
const activeRuns = activity.runs.filter((run) => isProcessRunning(run.pid));
|
|
58
|
+
const pruned = activeRuns.length !== activity.runs.length;
|
|
59
|
+
if (pruned) {
|
|
60
|
+
writeLocalghostActivity({ ...activity, version: LOCALGHOST_ACTIVITY_VERSION, runs: activeRuns }, path);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
path,
|
|
64
|
+
pruned,
|
|
65
|
+
runs: activeRuns,
|
|
66
|
+
setups: activity.setups
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function registerLocalghostRun(input2, path = getLocalghostActivityPath()) {
|
|
70
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
71
|
+
const pid = input2.pid ?? process.pid;
|
|
72
|
+
const record = {
|
|
73
|
+
id: input2.id ?? createRunId(input2, pid),
|
|
74
|
+
mode: input2.mode,
|
|
75
|
+
pid,
|
|
76
|
+
cwd: input2.cwd,
|
|
77
|
+
projectName: input2.projectName,
|
|
78
|
+
startedAt: input2.startedAt ?? now,
|
|
79
|
+
updatedAt: now,
|
|
80
|
+
...input2.configPath ? { configPath: input2.configPath } : {},
|
|
81
|
+
...input2.caddyfilePath ? { caddyfilePath: input2.caddyfilePath } : {},
|
|
82
|
+
...input2.caddyPid ? { caddyPid: input2.caddyPid } : {},
|
|
83
|
+
...input2.childPid ? { childPid: input2.childPid } : {},
|
|
84
|
+
...input2.childCommand ? { childCommand: input2.childCommand } : {},
|
|
85
|
+
...typeof input2.https === "boolean" ? { https: input2.https } : {},
|
|
86
|
+
...input2.requestedPort ? { requestedPort: input2.requestedPort } : {},
|
|
87
|
+
...input2.port ? { port: input2.port } : {},
|
|
88
|
+
...typeof input2.dynamicPort === "boolean" ? { dynamicPort: input2.dynamicPort } : {},
|
|
89
|
+
entries: input2.entries
|
|
90
|
+
};
|
|
91
|
+
const current = pruneLocalghostActivity(path).runs.filter((run) => run.id !== record.id);
|
|
92
|
+
const activity = readLocalghostActivity(path);
|
|
93
|
+
writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs: [...current, record], setups: activity.setups }, path);
|
|
94
|
+
return record;
|
|
95
|
+
}
|
|
96
|
+
function registerLocalghostSetup(input2, path = getLocalghostActivityPath()) {
|
|
97
|
+
const record = {
|
|
98
|
+
id: input2.id ?? createSetupId(input2),
|
|
99
|
+
cwd: input2.cwd,
|
|
100
|
+
projectName: input2.projectName,
|
|
101
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
102
|
+
...input2.configPath ? { configPath: input2.configPath } : {},
|
|
103
|
+
...input2.caddyfilePath ? { caddyfilePath: input2.caddyfilePath } : {},
|
|
104
|
+
...typeof input2.https === "boolean" ? { https: input2.https } : {},
|
|
105
|
+
entries: input2.entries
|
|
106
|
+
};
|
|
107
|
+
const activity = pruneLocalghostActivity(path);
|
|
108
|
+
const setups = activity.setups.filter((setup) => setup.id !== record.id);
|
|
109
|
+
writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs: activity.runs, setups: [...setups, record] }, path);
|
|
110
|
+
return record;
|
|
111
|
+
}
|
|
112
|
+
function unregisterLocalghostRun(id, path = getLocalghostActivityPath()) {
|
|
113
|
+
const activity = readLocalghostActivity(path);
|
|
114
|
+
const runs = activity.runs.filter((run) => run.id !== id);
|
|
115
|
+
if (runs.length !== activity.runs.length) {
|
|
116
|
+
writeLocalghostActivity({ ...activity, version: LOCALGHOST_ACTIVITY_VERSION, runs }, path);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
4
119
|
|
|
5
120
|
// src/config.ts
|
|
6
|
-
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
7
|
-
import { basename, join, resolve } from "path";
|
|
121
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
122
|
+
import { basename, join as join2, resolve } from "path";
|
|
8
123
|
|
|
9
124
|
// src/parse.ts
|
|
10
125
|
var HOST_PATTERN = /^(?=.{1,253}$)(?!-)[a-z0-9-]+(\.[a-z0-9-]+)*\.?$/i;
|
|
@@ -69,7 +184,7 @@ function resolveDevHostsPath(options = {}) {
|
|
|
69
184
|
const searchedFiles = getConfigFileCandidates(options);
|
|
70
185
|
for (const fileName2 of searchedFiles) {
|
|
71
186
|
const path = resolve(cwd, fileName2);
|
|
72
|
-
if (
|
|
187
|
+
if (existsSync2(path)) {
|
|
73
188
|
return {
|
|
74
189
|
path,
|
|
75
190
|
fileName: basename(fileName2),
|
|
@@ -102,11 +217,11 @@ function readDevHosts(options = {}) {
|
|
|
102
217
|
`Missing Localghost config in ${cwd}. Looked for ${formatSearchedFiles(resolvedPath.searchedFiles, resolvedPath.configPattern)}. Run \`localghost init\` or pass --config/--config-pattern.`
|
|
103
218
|
);
|
|
104
219
|
}
|
|
105
|
-
return parseDevHosts(
|
|
220
|
+
return parseDevHosts(readFileSync2(resolvedPath.path, "utf8"), resolvedPath.fileName);
|
|
106
221
|
}
|
|
107
222
|
function getProjectName(cwd = process.cwd()) {
|
|
108
223
|
try {
|
|
109
|
-
const pkg = JSON.parse(
|
|
224
|
+
const pkg = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
|
|
110
225
|
const name = typeof pkg.name === "string" && pkg.name ? pkg.name : "app";
|
|
111
226
|
return sanitizeProjectName(name.replace(/^@/, ""));
|
|
112
227
|
} catch {
|
|
@@ -119,7 +234,8 @@ function sanitizeProjectName(value) {
|
|
|
119
234
|
}
|
|
120
235
|
|
|
121
236
|
// src/context.ts
|
|
122
|
-
import { existsSync as
|
|
237
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
238
|
+
import { join as join3 } from "path";
|
|
123
239
|
import { pathToFileURL } from "url";
|
|
124
240
|
|
|
125
241
|
// src/port.ts
|
|
@@ -148,6 +264,261 @@ async function findAvailablePort(startPort, options = {}) {
|
|
|
148
264
|
throw new Error(`No available port found from ${startPort} to ${startPort + maxAttempts - 1}.`);
|
|
149
265
|
}
|
|
150
266
|
|
|
267
|
+
// src/tunnel.ts
|
|
268
|
+
import { domainToASCII } from "url";
|
|
269
|
+
var DEFAULT_GHOST_TUNNEL_SUBDOMAIN = "ghost";
|
|
270
|
+
var DEFAULT_GHOST_TUNNEL_NAMESPACE_TAGS = ["route", "project", "owner"];
|
|
271
|
+
var DEFAULT_GHOST_TUNNEL_NAMESPACE_SEPARATOR = "-";
|
|
272
|
+
var DEFAULT_GHOST_TUNNEL_MODE = "manual";
|
|
273
|
+
function isResolvedGhostTunnelConfig(value) {
|
|
274
|
+
return typeof value === "object" && value !== null && "enabled" in value;
|
|
275
|
+
}
|
|
276
|
+
function toGhostTunnelConfig(options) {
|
|
277
|
+
return isResolvedGhostTunnelConfig(options) ? options : resolveGhostTunnelConfig(options);
|
|
278
|
+
}
|
|
279
|
+
function stripHostPort(value) {
|
|
280
|
+
const trimmed = value.trim().toLowerCase();
|
|
281
|
+
if (trimmed.startsWith("[") || trimmed.includes("/")) return "";
|
|
282
|
+
const portSeparator = trimmed.lastIndexOf(":");
|
|
283
|
+
if (portSeparator === -1) return trimmed;
|
|
284
|
+
const port = trimmed.slice(portSeparator + 1);
|
|
285
|
+
return /^\d+$/.test(port) ? trimmed.slice(0, portSeparator) : trimmed;
|
|
286
|
+
}
|
|
287
|
+
function normalizeDomain(value) {
|
|
288
|
+
const host = stripHostPort(value.replace(/^\*\./, ""));
|
|
289
|
+
const ascii = domainToASCII(host);
|
|
290
|
+
if (!ascii || ascii.length > 253 || ascii.includes("..")) return null;
|
|
291
|
+
if (ascii.startsWith(".") || ascii.endsWith(".")) return null;
|
|
292
|
+
if (ascii.includes("*")) return null;
|
|
293
|
+
if (!ascii.split(".").every(isValidHostLabel)) return null;
|
|
294
|
+
return ascii;
|
|
295
|
+
}
|
|
296
|
+
function isValidHostLabel(value) {
|
|
297
|
+
return value.length > 0 && value.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(value);
|
|
298
|
+
}
|
|
299
|
+
function isValidNamespaceTag(value) {
|
|
300
|
+
return /^[a-z][a-z0-9_]*$/i.test(value);
|
|
301
|
+
}
|
|
302
|
+
function isNamespaceTagList(options) {
|
|
303
|
+
return Array.isArray(options);
|
|
304
|
+
}
|
|
305
|
+
function assertValidSubdomain(value) {
|
|
306
|
+
if (!isValidHostLabel(value)) {
|
|
307
|
+
throw new Error(`Invalid ghost tunnel subdomain: ${value}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function normalizeDomains(domains) {
|
|
311
|
+
const values = typeof domains === "string" ? [domains] : [...domains ?? []];
|
|
312
|
+
const normalized = values.map((value) => value.trim()).filter(Boolean).map((value) => {
|
|
313
|
+
const domain = normalizeDomain(value);
|
|
314
|
+
if (!domain) throw new Error(`Invalid ghost tunnel domain: ${value}`);
|
|
315
|
+
return domain;
|
|
316
|
+
});
|
|
317
|
+
return [...new Set(normalized)];
|
|
318
|
+
}
|
|
319
|
+
function parseGhostTunnelMode(value) {
|
|
320
|
+
return value ?? DEFAULT_GHOST_TUNNEL_MODE;
|
|
321
|
+
}
|
|
322
|
+
function resolveNamespaceConfig(options) {
|
|
323
|
+
const tags = isNamespaceTagList(options) ? [...options] : [...options?.tags ?? DEFAULT_GHOST_TUNNEL_NAMESPACE_TAGS];
|
|
324
|
+
let separator = DEFAULT_GHOST_TUNNEL_NAMESPACE_SEPARATOR;
|
|
325
|
+
let spreadTag = tags.includes("project") ? "project" : void 0;
|
|
326
|
+
if (options && !isNamespaceTagList(options)) {
|
|
327
|
+
separator = options.separator ?? DEFAULT_GHOST_TUNNEL_NAMESPACE_SEPARATOR;
|
|
328
|
+
spreadTag = options.spreadTag === false ? false : options.spreadTag ?? spreadTag;
|
|
329
|
+
}
|
|
330
|
+
if (tags.length === 0) {
|
|
331
|
+
throw new Error("Ghost tunnel namespace must include at least one tag.");
|
|
332
|
+
}
|
|
333
|
+
for (const tag of tags) {
|
|
334
|
+
if (!isValidNamespaceTag(tag)) {
|
|
335
|
+
throw new Error(`Invalid ghost tunnel namespace tag: ${tag}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (spreadTag && !tags.includes(spreadTag)) {
|
|
339
|
+
throw new Error(`Ghost tunnel namespace spreadTag must be listed in tags: ${spreadTag}`);
|
|
340
|
+
}
|
|
341
|
+
if (!separator || separator.length > 8 || !/^[a-z0-9-]+$/.test(separator)) {
|
|
342
|
+
throw new Error(`Invalid ghost tunnel namespace separator: ${separator}`);
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
tags,
|
|
346
|
+
separator,
|
|
347
|
+
...spreadTag ? { spreadTag } : {}
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function normalizeNamespaceValue(tag, value, separator, options = {}) {
|
|
351
|
+
const normalized = normalizeDomain(value);
|
|
352
|
+
if (!normalized || normalized.includes(".")) {
|
|
353
|
+
throw new Error(`Invalid ghost tunnel namespace value for ${tag}: ${value}`);
|
|
354
|
+
}
|
|
355
|
+
if (!options.allowSeparator && normalized.includes(separator)) {
|
|
356
|
+
throw new Error(`Ghost tunnel namespace value for ${tag} cannot include separator "${separator}": ${value}`);
|
|
357
|
+
}
|
|
358
|
+
return normalized;
|
|
359
|
+
}
|
|
360
|
+
function createNamespaceSlug(config, values) {
|
|
361
|
+
const parts = config.tags.map((tag) => {
|
|
362
|
+
const value = values[tag];
|
|
363
|
+
if (!value) {
|
|
364
|
+
throw new Error(`Missing ghost tunnel namespace value: ${tag}`);
|
|
365
|
+
}
|
|
366
|
+
return normalizeNamespaceValue(tag, value, config.separator, { allowSeparator: tag === config.spreadTag });
|
|
367
|
+
});
|
|
368
|
+
const slug = parts.join(config.separator);
|
|
369
|
+
if (!isValidHostLabel(slug)) {
|
|
370
|
+
throw new Error(`Ghost tunnel namespace is too long for a DNS label: ${slug}`);
|
|
371
|
+
}
|
|
372
|
+
return slug;
|
|
373
|
+
}
|
|
374
|
+
function createNamespaceDisplaySlug(config, values = {}) {
|
|
375
|
+
return config.tags.map((tag) => {
|
|
376
|
+
const value = values[tag];
|
|
377
|
+
if (!value) return `<${tag}>`;
|
|
378
|
+
try {
|
|
379
|
+
return normalizeNamespaceValue(tag, value, config.separator, { allowSeparator: tag === config.spreadTag });
|
|
380
|
+
} catch {
|
|
381
|
+
return `<${tag}>`;
|
|
382
|
+
}
|
|
383
|
+
}).join(config.separator);
|
|
384
|
+
}
|
|
385
|
+
function getPreviewDefaults(preview, defaults) {
|
|
386
|
+
return {
|
|
387
|
+
domain: preview?.domain ?? defaults?.domain,
|
|
388
|
+
route: preview?.route ?? defaults?.route,
|
|
389
|
+
project: preview?.project ?? defaults?.project,
|
|
390
|
+
owner: preview?.owner ?? defaults?.owner,
|
|
391
|
+
values: {
|
|
392
|
+
...defaults?.values ?? {},
|
|
393
|
+
...preview?.values ?? {}
|
|
394
|
+
},
|
|
395
|
+
path: preview?.path,
|
|
396
|
+
protocol: preview?.protocol
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function getDisplayValues(input2) {
|
|
400
|
+
return {
|
|
401
|
+
...input2.route ? { route: input2.route } : {},
|
|
402
|
+
...input2.project ? { project: input2.project } : {},
|
|
403
|
+
...input2.owner ? { owner: input2.owner } : {},
|
|
404
|
+
...input2.values
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function getDisplayDefaults(config, defaults) {
|
|
408
|
+
return config.mode === "public" && !config.preview ? void 0 : defaults;
|
|
409
|
+
}
|
|
410
|
+
function createDisplayUrl(config, defaults, domain) {
|
|
411
|
+
const input2 = getPreviewDefaults(config.preview, getDisplayDefaults(config, defaults));
|
|
412
|
+
const protocol = input2.protocol ?? "https";
|
|
413
|
+
const slug = createNamespaceDisplaySlug(config.namespace, getDisplayValues(input2));
|
|
414
|
+
const entryHost = domain ? getGhostTunnelEntryHost(domain, config) : input2.domain ? getGhostTunnelEntryHost(input2.domain, config) : `${config.subdomain}.*`;
|
|
415
|
+
const url = `${protocol}://${slug}.${entryHost}/`;
|
|
416
|
+
if (!input2.path) return url;
|
|
417
|
+
return `${url}${input2.path.replace(/^\/+/, "")}`;
|
|
418
|
+
}
|
|
419
|
+
function createDisplayUrls(config, defaults) {
|
|
420
|
+
const displayDefaults = getDisplayDefaults(config, defaults);
|
|
421
|
+
const domains = config.domains.length > 0 ? config.domains : displayDefaults?.domain ? [displayDefaults.domain] : [];
|
|
422
|
+
const urls = domains.length > 0 ? domains.map((domain) => createDisplayUrl(config, displayDefaults, domain)) : [createDisplayUrl(config, displayDefaults)];
|
|
423
|
+
return [...new Set(urls)];
|
|
424
|
+
}
|
|
425
|
+
function maybeConstructPreviewUrl(config, defaults) {
|
|
426
|
+
if (!config.preview) return void 0;
|
|
427
|
+
const input2 = getPreviewDefaults(config.preview, defaults);
|
|
428
|
+
if (!input2.domain || !input2.route || !input2.project || !input2.owner) return void 0;
|
|
429
|
+
return constructGhostTunnelUrl({
|
|
430
|
+
domain: input2.domain,
|
|
431
|
+
route: input2.route,
|
|
432
|
+
project: input2.project,
|
|
433
|
+
owner: input2.owner,
|
|
434
|
+
values: input2.values,
|
|
435
|
+
...input2.path ? { path: input2.path } : {},
|
|
436
|
+
...input2.protocol ? { protocol: input2.protocol } : {},
|
|
437
|
+
ghostTunnel: config
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
function resolveGhostTunnelConfig(options, defaults) {
|
|
441
|
+
if (options === false || typeof options === "undefined") {
|
|
442
|
+
return {
|
|
443
|
+
enabled: false,
|
|
444
|
+
mode: DEFAULT_GHOST_TUNNEL_MODE,
|
|
445
|
+
domains: [],
|
|
446
|
+
subdomain: DEFAULT_GHOST_TUNNEL_SUBDOMAIN,
|
|
447
|
+
namespace: resolveNamespaceConfig(void 0),
|
|
448
|
+
displayUrls: [],
|
|
449
|
+
requireHttps: true,
|
|
450
|
+
requireAuth: true
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
const config = typeof options === "string" ? { mode: options } : options;
|
|
454
|
+
const subdomain = config.subdomain ?? DEFAULT_GHOST_TUNNEL_SUBDOMAIN;
|
|
455
|
+
assertValidSubdomain(subdomain);
|
|
456
|
+
const domains = normalizeDomains(config.domains);
|
|
457
|
+
const enabled = config.enabled ?? true;
|
|
458
|
+
const resolved = {
|
|
459
|
+
enabled,
|
|
460
|
+
mode: parseGhostTunnelMode(config.mode),
|
|
461
|
+
domains,
|
|
462
|
+
subdomain,
|
|
463
|
+
namespace: resolveNamespaceConfig(config.namespace),
|
|
464
|
+
...config.preview ? { preview: config.preview } : {},
|
|
465
|
+
displayUrls: [],
|
|
466
|
+
requireHttps: config.requireHttps ?? true,
|
|
467
|
+
requireAuth: config.requireAuth ?? true
|
|
468
|
+
};
|
|
469
|
+
if (!enabled) {
|
|
470
|
+
return resolved;
|
|
471
|
+
}
|
|
472
|
+
const previewUrl = maybeConstructPreviewUrl(resolved, defaults);
|
|
473
|
+
const displayUrls = previewUrl ? [previewUrl] : createDisplayUrls(resolved, defaults);
|
|
474
|
+
return {
|
|
475
|
+
...resolved,
|
|
476
|
+
displayUrls,
|
|
477
|
+
...displayUrls[0] ? { displayUrl: displayUrls[0] } : {},
|
|
478
|
+
...previewUrl ? { previewUrl } : {}
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function getGhostTunnelEntryHost(domain, options = {}) {
|
|
482
|
+
const config = toGhostTunnelConfig(options);
|
|
483
|
+
const normalizedDomain = normalizeDomain(domain);
|
|
484
|
+
if (!normalizedDomain) {
|
|
485
|
+
throw new Error(`Invalid ghost tunnel domain: ${domain}`);
|
|
486
|
+
}
|
|
487
|
+
return `${config.subdomain}.${normalizedDomain}`;
|
|
488
|
+
}
|
|
489
|
+
function constructGhostTunnelHost(input2) {
|
|
490
|
+
const config = toGhostTunnelConfig(input2.ghostTunnel ?? {});
|
|
491
|
+
if (!config.enabled) {
|
|
492
|
+
throw new Error("Ghost tunnel is not enabled.");
|
|
493
|
+
}
|
|
494
|
+
const namespaceValues = {
|
|
495
|
+
route: input2.route,
|
|
496
|
+
project: input2.project,
|
|
497
|
+
owner: input2.owner,
|
|
498
|
+
...input2.values ?? {}
|
|
499
|
+
};
|
|
500
|
+
const slug = createNamespaceSlug(config.namespace, namespaceValues);
|
|
501
|
+
return `${slug}.${getGhostTunnelEntryHost(input2.domain, config)}`;
|
|
502
|
+
}
|
|
503
|
+
function constructGhostTunnelUrl(input2) {
|
|
504
|
+
const protocol = input2.protocol ?? "https";
|
|
505
|
+
const host = constructGhostTunnelHost(input2);
|
|
506
|
+
const url = new URL(`${protocol}://${host}/`);
|
|
507
|
+
if (input2.path) {
|
|
508
|
+
url.pathname = `/${input2.path.replace(/^\/+/, "")}`;
|
|
509
|
+
}
|
|
510
|
+
if (input2.searchParams instanceof URLSearchParams) {
|
|
511
|
+
url.search = input2.searchParams.toString();
|
|
512
|
+
} else if (input2.searchParams) {
|
|
513
|
+
for (const [key, value] of Object.entries(input2.searchParams)) {
|
|
514
|
+
if (typeof value !== "undefined" && value !== null) {
|
|
515
|
+
url.searchParams.set(key, String(value));
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return url.toString();
|
|
520
|
+
}
|
|
521
|
+
|
|
151
522
|
// src/context.ts
|
|
152
523
|
var LOCALGHOST_PROJECT_CONFIG_FILES = [
|
|
153
524
|
"localghost.config.mjs",
|
|
@@ -172,6 +543,25 @@ function envHttps() {
|
|
|
172
543
|
if (!value) return void 0;
|
|
173
544
|
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
174
545
|
}
|
|
546
|
+
function getPackageName(cwd) {
|
|
547
|
+
try {
|
|
548
|
+
const pkg = JSON.parse(readFileSync3(join3(cwd, "package.json"), "utf8"));
|
|
549
|
+
return typeof pkg.name === "string" ? pkg.name : void 0;
|
|
550
|
+
} catch {
|
|
551
|
+
return void 0;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function getPackageOwner(cwd) {
|
|
555
|
+
const packageName = getPackageName(cwd);
|
|
556
|
+
if (!packageName?.startsWith("@")) return void 0;
|
|
557
|
+
return packageName.slice(1).split("/")[0];
|
|
558
|
+
}
|
|
559
|
+
function getLocalOwner(cwd) {
|
|
560
|
+
return sanitizeProjectName(process.env.LOCALGHOST_OWNER ?? getPackageOwner(cwd) ?? process.env.USER ?? process.env.USERNAME ?? "local");
|
|
561
|
+
}
|
|
562
|
+
function getRouteName(primaryHost, fallback) {
|
|
563
|
+
return sanitizeProjectName(primaryHost.split(".")[0] ?? fallback);
|
|
564
|
+
}
|
|
175
565
|
function readOptionsFromContext(options) {
|
|
176
566
|
return {
|
|
177
567
|
cwd: options.cwd ?? process.cwd(),
|
|
@@ -210,18 +600,22 @@ function addDefaultWwwAliases(entries) {
|
|
|
210
600
|
function defined(input2) {
|
|
211
601
|
return Object.fromEntries(Object.entries(input2).filter(([, value]) => typeof value !== "undefined"));
|
|
212
602
|
}
|
|
213
|
-
async function
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
603
|
+
async function readLocalghostProjectConfig(options = {}) {
|
|
604
|
+
const cwd = options.cwd ?? process.cwd();
|
|
605
|
+
if (options.configFile === false) return { config: {} };
|
|
606
|
+
const candidates = options.configFile ? [options.configFile] : LOCALGHOST_PROJECT_CONFIG_FILES;
|
|
607
|
+
const path = candidates.map((candidate) => resolveDevHostsPath({ cwd, fileName: candidate }).path).find((candidate) => existsSync3(candidate));
|
|
608
|
+
if (!path) return { config: {} };
|
|
218
609
|
const imported = await import(`${pathToFileURL(path).href}?localghost=${Date.now()}`);
|
|
219
610
|
const config = imported.default ?? imported;
|
|
220
611
|
return { config, path };
|
|
221
612
|
}
|
|
222
613
|
async function resolveLocalghostContext(options = {}) {
|
|
223
614
|
const cwd = options.cwd ?? process.cwd();
|
|
224
|
-
const projectConfig = await
|
|
615
|
+
const projectConfig = await readLocalghostProjectConfig({
|
|
616
|
+
cwd,
|
|
617
|
+
...typeof options.localghostConfig !== "undefined" ? { configFile: options.localghostConfig } : {}
|
|
618
|
+
});
|
|
225
619
|
const merged = {
|
|
226
620
|
...projectConfig.config,
|
|
227
621
|
...defined(options)
|
|
@@ -230,7 +624,7 @@ async function resolveLocalghostContext(options = {}) {
|
|
|
230
624
|
const resolvedPath = resolveDevHostsPath(readOptions);
|
|
231
625
|
const configEntries = readDevHosts(readOptions);
|
|
232
626
|
const requestedPort = merged.port ?? envPort() ?? configEntries[0]?.port ?? 5173;
|
|
233
|
-
const dynamicPort = merged.dynamicPort ?? envDynamicPort() ??
|
|
627
|
+
const dynamicPort = merged.dynamicPort ?? envDynamicPort() ?? true;
|
|
234
628
|
const bindHost = merged.bindHost ?? "127.0.0.1";
|
|
235
629
|
const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
|
|
236
630
|
const port = dynamicPort ? await findAvailablePort(requestedPort, { host: probeHost }) : requestedPort;
|
|
@@ -238,9 +632,15 @@ async function resolveLocalghostContext(options = {}) {
|
|
|
238
632
|
const entries = wwwAlias ? addDefaultWwwAliases(withRuntimePort(configEntries, requestedPort, port)) : withRuntimePort(configEntries, requestedPort, port);
|
|
239
633
|
const hosts = uniqueHosts(entries);
|
|
240
634
|
const primaryHost = merged.primaryHost ?? entries.find((entry) => entry.port === port)?.host ?? hosts[0] ?? `${sanitizeProjectName(getProjectName(cwd))}.localhost`;
|
|
635
|
+
const projectName = sanitizeProjectName(merged.project ?? getProjectName(cwd));
|
|
636
|
+
const ghostTunnel = resolveGhostTunnelConfig(merged.ghostTunnel, {
|
|
637
|
+
route: getRouteName(primaryHost, projectName),
|
|
638
|
+
project: projectName,
|
|
639
|
+
owner: getLocalOwner(cwd)
|
|
640
|
+
});
|
|
241
641
|
return {
|
|
242
642
|
cwd,
|
|
243
|
-
projectName
|
|
643
|
+
projectName,
|
|
244
644
|
readOptions,
|
|
245
645
|
configPath: resolvedPath.path,
|
|
246
646
|
configFileName: resolvedPath.fileName,
|
|
@@ -254,6 +654,7 @@ async function resolveLocalghostContext(options = {}) {
|
|
|
254
654
|
primaryHost,
|
|
255
655
|
https: merged.https ?? envHttps() ?? false,
|
|
256
656
|
wwwAlias,
|
|
657
|
+
ghostTunnel,
|
|
257
658
|
...projectConfig.path ? { projectConfigPath: projectConfig.path } : {}
|
|
258
659
|
};
|
|
259
660
|
}
|
|
@@ -293,21 +694,21 @@ function isProductionLike(env = process.env) {
|
|
|
293
694
|
}
|
|
294
695
|
|
|
295
696
|
// src/fs.ts
|
|
296
|
-
import { mkdirSync, readFileSync as
|
|
297
|
-
import { dirname } from "path";
|
|
697
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
698
|
+
import { dirname as dirname2 } from "path";
|
|
298
699
|
function readTextFile(path) {
|
|
299
|
-
return
|
|
700
|
+
return readFileSync4(path, "utf8");
|
|
300
701
|
}
|
|
301
702
|
function writeTextFile(path, value) {
|
|
302
|
-
|
|
303
|
-
|
|
703
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
704
|
+
writeFileSync2(path, value, "utf8");
|
|
304
705
|
return path;
|
|
305
706
|
}
|
|
306
707
|
|
|
307
708
|
// src/hosts-file.ts
|
|
308
|
-
import { writeFileSync as
|
|
709
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
309
710
|
import { tmpdir } from "os";
|
|
310
|
-
import { join as
|
|
711
|
+
import { join as join4 } from "path";
|
|
311
712
|
import { execa as execa2 } from "execa";
|
|
312
713
|
function escapeRegExp(value) {
|
|
313
714
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -342,8 +743,8 @@ ${block}`;
|
|
|
342
743
|
}
|
|
343
744
|
async function writeSystemHostsFile(hostsPath, next, projectName) {
|
|
344
745
|
const sanitizedProjectName = sanitizeProjectName(projectName);
|
|
345
|
-
const tempPath =
|
|
346
|
-
|
|
746
|
+
const tempPath = join4(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
|
|
747
|
+
writeFileSync3(tempPath, next, "utf8");
|
|
347
748
|
if (process.platform === "win32") {
|
|
348
749
|
throw new Error(`Windows support: run as administrator and copy ${tempPath} to ${hostsPath}.`);
|
|
349
750
|
}
|
|
@@ -394,15 +795,15 @@ async function ask(question, defaultValue) {
|
|
|
394
795
|
}
|
|
395
796
|
|
|
396
797
|
// src/state.ts
|
|
397
|
-
import { existsSync as
|
|
398
|
-
import { join as
|
|
798
|
+
import { existsSync as existsSync4 } from "fs";
|
|
799
|
+
import { join as join5 } from "path";
|
|
399
800
|
var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
|
|
400
801
|
function getLocalghostStatePath(cwd = process.cwd()) {
|
|
401
|
-
return
|
|
802
|
+
return join5(cwd, LOCALGHOST_STATE_FILE);
|
|
402
803
|
}
|
|
403
804
|
function readLocalghostState(cwd = process.cwd()) {
|
|
404
805
|
const path = getLocalghostStatePath(cwd);
|
|
405
|
-
if (!
|
|
806
|
+
if (!existsSync4(path)) return null;
|
|
406
807
|
return JSON.parse(readTextFile(path));
|
|
407
808
|
}
|
|
408
809
|
function writeLocalghostState(cwd, state) {
|
|
@@ -413,7 +814,7 @@ function writeLocalghostState(cwd, state) {
|
|
|
413
814
|
}
|
|
414
815
|
|
|
415
816
|
// src/caddy.ts
|
|
416
|
-
import { dirname as
|
|
817
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
417
818
|
import { execa as execa3 } from "execa";
|
|
418
819
|
function groupByPort(entries) {
|
|
419
820
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -425,7 +826,7 @@ function groupByPort(entries) {
|
|
|
425
826
|
return groups;
|
|
426
827
|
}
|
|
427
828
|
function getCaddyfilePath(cwd = process.cwd()) {
|
|
428
|
-
return
|
|
829
|
+
return join6(cwd, "ops/local/Caddyfile");
|
|
429
830
|
}
|
|
430
831
|
function renderCaddyfile(entries, options = {}) {
|
|
431
832
|
const groups = groupByPort(entries);
|
|
@@ -451,11 +852,54 @@ async function writeCaddyfile(entries, cwd = process.cwd(), options = {}) {
|
|
|
451
852
|
}
|
|
452
853
|
async function validateCaddyfile(path) {
|
|
453
854
|
await execa3("caddy", ["validate", "--config", path], {
|
|
454
|
-
cwd:
|
|
855
|
+
cwd: dirname3(path),
|
|
455
856
|
stdio: "inherit"
|
|
456
857
|
});
|
|
457
858
|
}
|
|
458
859
|
|
|
860
|
+
// src/routes.ts
|
|
861
|
+
var ansi = {
|
|
862
|
+
cyan: "\x1B[36m",
|
|
863
|
+
dim: "\x1B[2m",
|
|
864
|
+
green: "\x1B[32m",
|
|
865
|
+
reset: "\x1B[0m",
|
|
866
|
+
yellow: "\x1B[33m"
|
|
867
|
+
};
|
|
868
|
+
function colorize(value, color, enabled) {
|
|
869
|
+
return enabled ? `${color}${value}${ansi.reset}` : value;
|
|
870
|
+
}
|
|
871
|
+
function colorizeUrl(value, enabled) {
|
|
872
|
+
if (!enabled) return value;
|
|
873
|
+
return colorize(value.replace(/\*/g, `${ansi.yellow}*${ansi.cyan}`), ansi.cyan, enabled);
|
|
874
|
+
}
|
|
875
|
+
function formatGhostTunnel(config, options = {}) {
|
|
876
|
+
if (!config.enabled) return null;
|
|
877
|
+
const color = options.color === true;
|
|
878
|
+
const label = options.label ?? "expected";
|
|
879
|
+
const labelColor = label === "running" ? ansi.green : ansi.dim;
|
|
880
|
+
const lines = [
|
|
881
|
+
"localghost ghost tunnel",
|
|
882
|
+
` mode: ${config.mode}`
|
|
883
|
+
];
|
|
884
|
+
const urls = config.displayUrls.length > 0 ? config.displayUrls : config.displayUrl ? [config.displayUrl] : [];
|
|
885
|
+
if (urls.length === 0) {
|
|
886
|
+
lines.push(` ${label}: unavailable`);
|
|
887
|
+
} else if (urls.length === 1) {
|
|
888
|
+
lines.push(` ${colorize(label, labelColor, color)}: ${colorizeUrl(urls[0], color)}`);
|
|
889
|
+
} else {
|
|
890
|
+
lines.push(` ${colorize(label, labelColor, color)}:`);
|
|
891
|
+
for (const url of urls) {
|
|
892
|
+
lines.push(` ${colorizeUrl(url, color)}`);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (options.verbose) {
|
|
896
|
+
lines.push(` domains: ${config.domains.length > 0 ? config.domains.join(", ") : "*"}`);
|
|
897
|
+
lines.push(` access: ${config.requireAuth ? "auth required" : "app decides"}`);
|
|
898
|
+
lines.push(` transport: ${config.requireHttps ? "https required" : "http allowed"}`);
|
|
899
|
+
}
|
|
900
|
+
return lines.join("\n");
|
|
901
|
+
}
|
|
902
|
+
|
|
459
903
|
// src/vite.ts
|
|
460
904
|
function mergeAllowedHosts(current, hosts) {
|
|
461
905
|
if (Array.isArray(current)) {
|
|
@@ -470,7 +914,11 @@ function getDisplayEntries(entries, vitePort) {
|
|
|
470
914
|
const matchingEntries = entries.filter((entry) => entry.port === vitePort);
|
|
471
915
|
return matchingEntries.length > 0 ? matchingEntries : entries;
|
|
472
916
|
}
|
|
473
|
-
function printLocalHosts(server,
|
|
917
|
+
function printLocalHosts(server, context) {
|
|
918
|
+
if (!context) return;
|
|
919
|
+
const entries = context.entries;
|
|
920
|
+
const vitePort = context.port;
|
|
921
|
+
const https = context.https;
|
|
474
922
|
const displayEntries = getDisplayEntries(entries, vitePort);
|
|
475
923
|
const protocol = https ? "https" : "http";
|
|
476
924
|
const urls = displayEntries.map((entry) => `${protocol}://${entry.host}/`);
|
|
@@ -484,13 +932,25 @@ function printLocalHosts(server, entries, vitePort, https) {
|
|
|
484
932
|
` local: ${primaryUrl}`,
|
|
485
933
|
...urls.slice(1).map((url) => ` also: ${url}`),
|
|
486
934
|
vitePort ? ` target: http://127.0.0.1:${vitePort}/` : void 0,
|
|
487
|
-
https ? " proxy: Caddy local HTTPS" : void 0
|
|
935
|
+
https ? " proxy: Caddy local HTTPS" : void 0,
|
|
936
|
+
context.ghostTunnel.enabled ? formatGhostTunnel(context.ghostTunnel, {
|
|
937
|
+
color: shouldColor(),
|
|
938
|
+
label: "ready",
|
|
939
|
+
verbose: optionsVerbose(context)
|
|
940
|
+
}) : void 0,
|
|
941
|
+
process.stdin.isTTY && context.ghostTunnel.enabled ? " help: press h + enter for Vite, g + enter for Localghost" : void 0
|
|
488
942
|
].filter((line) => Boolean(line));
|
|
489
943
|
server.config.logger.info(lines.join("\n"), {
|
|
490
944
|
clear: false,
|
|
491
945
|
timestamp: false
|
|
492
946
|
});
|
|
493
947
|
}
|
|
948
|
+
function shouldColor() {
|
|
949
|
+
return process.stdout.isTTY && !process.env.NO_COLOR;
|
|
950
|
+
}
|
|
951
|
+
function optionsVerbose(context) {
|
|
952
|
+
return process.env.LOCALGHOST_VERBOSE === "1" || process.env.LOCALGHOST_VERBOSE === "true" || context.ghostTunnel.displayUrls.length > 1;
|
|
953
|
+
}
|
|
494
954
|
function readOptionsFromPlugin(options) {
|
|
495
955
|
return {
|
|
496
956
|
cwd: options.cwd ?? process.cwd(),
|
|
@@ -522,6 +982,57 @@ function defaultHost(cwd) {
|
|
|
522
982
|
const projectName = sanitizeProjectName(getProjectName(cwd).split("/").pop() ?? "app");
|
|
523
983
|
return `${projectName}.localhost`;
|
|
524
984
|
}
|
|
985
|
+
function getPackageOwner2(cwd) {
|
|
986
|
+
try {
|
|
987
|
+
const pkg = JSON.parse(readFileSync5(resolve2(cwd, "package.json"), "utf8"));
|
|
988
|
+
if (typeof pkg.name === "string" && pkg.name.startsWith("@")) {
|
|
989
|
+
return pkg.name.slice(1).split("/")[0];
|
|
990
|
+
}
|
|
991
|
+
} catch {
|
|
992
|
+
return void 0;
|
|
993
|
+
}
|
|
994
|
+
return void 0;
|
|
995
|
+
}
|
|
996
|
+
function getLocalOwner2(cwd) {
|
|
997
|
+
return sanitizeProjectName(process.env.LOCALGHOST_OWNER ?? getPackageOwner2(cwd) ?? process.env.USER ?? process.env.USERNAME ?? "local");
|
|
998
|
+
}
|
|
999
|
+
function getRouteName2(primaryHost, fallback) {
|
|
1000
|
+
return sanitizeProjectName(primaryHost.split(".")[0] ?? fallback);
|
|
1001
|
+
}
|
|
1002
|
+
function readBuildEntries(options) {
|
|
1003
|
+
const readOptions = readOptionsFromPlugin(options);
|
|
1004
|
+
const resolved = resolveDevHostsPath(readOptions);
|
|
1005
|
+
if (!resolved.exists) return [];
|
|
1006
|
+
try {
|
|
1007
|
+
return readDevHosts(readOptions);
|
|
1008
|
+
} catch {
|
|
1009
|
+
return [];
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
async function maybePrintBuildGhostTunnel(options) {
|
|
1013
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1014
|
+
const projectConfig = await readLocalghostProjectConfig({
|
|
1015
|
+
cwd,
|
|
1016
|
+
...typeof options.localghostConfig !== "undefined" ? { configFile: options.localghostConfig } : {}
|
|
1017
|
+
});
|
|
1018
|
+
const explicitGhostTunnel = typeof options.ghostTunnel !== "undefined" ? options.ghostTunnel : projectConfig.config.ghostTunnel;
|
|
1019
|
+
if (!explicitGhostTunnel) return;
|
|
1020
|
+
const entries = readBuildEntries(options);
|
|
1021
|
+
const projectName = sanitizeProjectName(projectConfig.config.project ?? getProjectName(cwd));
|
|
1022
|
+
const primaryHost = options.primaryHost ?? entries[0]?.host ?? defaultHost(cwd);
|
|
1023
|
+
const ghostTunnel = resolveGhostTunnelConfig(explicitGhostTunnel, {
|
|
1024
|
+
route: getRouteName2(primaryHost, projectName),
|
|
1025
|
+
project: sanitizeProjectName(projectConfig.config.project ?? getProjectName(cwd)),
|
|
1026
|
+
owner: getLocalOwner2(cwd)
|
|
1027
|
+
});
|
|
1028
|
+
if (!ghostTunnel.enabled) return;
|
|
1029
|
+
const formatted = formatGhostTunnel(ghostTunnel, {
|
|
1030
|
+
color: shouldColor(),
|
|
1031
|
+
label: "configured",
|
|
1032
|
+
verbose: options.verbose === true || process.env.LOCALGHOST_VERBOSE === "1" || process.env.LOCALGHOST_VERBOSE === "true"
|
|
1033
|
+
});
|
|
1034
|
+
if (formatted) console.log(formatted);
|
|
1035
|
+
}
|
|
525
1036
|
async function promptForHosts(cwd, port) {
|
|
526
1037
|
const primaryHost = await ask("Primary local domain", defaultHost(cwd));
|
|
527
1038
|
const hosts = [primaryHost.toLowerCase()];
|
|
@@ -536,13 +1047,13 @@ function hasReadySetup(cwd, entries, configPath, https) {
|
|
|
536
1047
|
const projectName = sanitizeProjectName(getProjectName(cwd));
|
|
537
1048
|
if (state?.action !== "setup" || state.configPath !== configPath) return false;
|
|
538
1049
|
try {
|
|
539
|
-
const hosts =
|
|
1050
|
+
const hosts = readFileSync5(getSystemHostsPath(), "utf8");
|
|
540
1051
|
if (!hosts.includes(renderHostsBlock(projectName, entries).trimEnd())) return false;
|
|
541
1052
|
} catch {
|
|
542
1053
|
return false;
|
|
543
1054
|
}
|
|
544
1055
|
const caddyfilePath = getCaddyfilePath(cwd);
|
|
545
|
-
return
|
|
1056
|
+
return existsSync5(caddyfilePath) && readFileSync5(caddyfilePath, "utf8") === renderCaddyfile(entries, { https });
|
|
546
1057
|
}
|
|
547
1058
|
async function setupProject(cwd, entries, configPath, https) {
|
|
548
1059
|
const caddy = await checkCaddy();
|
|
@@ -571,6 +1082,14 @@ async function setupProject(cwd, entries, configPath, https) {
|
|
|
571
1082
|
caddyHttps: https,
|
|
572
1083
|
entries
|
|
573
1084
|
});
|
|
1085
|
+
registerLocalghostSetup({
|
|
1086
|
+
cwd,
|
|
1087
|
+
projectName,
|
|
1088
|
+
configPath,
|
|
1089
|
+
caddyfilePath,
|
|
1090
|
+
https,
|
|
1091
|
+
entries
|
|
1092
|
+
});
|
|
574
1093
|
}
|
|
575
1094
|
async function ensureLocalghostContext(options, vitePort, https) {
|
|
576
1095
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -606,16 +1125,91 @@ async function ensureLocalghostContext(options, vitePort, https) {
|
|
|
606
1125
|
}
|
|
607
1126
|
return context;
|
|
608
1127
|
}
|
|
1128
|
+
function isConcreteGhostUrl(url) {
|
|
1129
|
+
return !url.includes("*") && !url.includes("<") && /^https?:\/\//.test(url);
|
|
1130
|
+
}
|
|
1131
|
+
function getConcreteGhostUrls(context) {
|
|
1132
|
+
return context.ghostTunnel.displayUrls.filter(isConcreteGhostUrl);
|
|
1133
|
+
}
|
|
1134
|
+
function openExternalUrl(url) {
|
|
1135
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
1136
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
1137
|
+
const child = spawn(command, args, {
|
|
1138
|
+
detached: true,
|
|
1139
|
+
stdio: "ignore"
|
|
1140
|
+
});
|
|
1141
|
+
child.unref();
|
|
1142
|
+
}
|
|
1143
|
+
function installGhostTunnelMenu(server, context) {
|
|
1144
|
+
if (!context?.ghostTunnel.enabled || !process.stdin.isTTY) return void 0;
|
|
1145
|
+
emitKeypressEvents(process.stdin);
|
|
1146
|
+
let active = false;
|
|
1147
|
+
const concreteUrls = getConcreteGhostUrls(context);
|
|
1148
|
+
const printMenu = () => {
|
|
1149
|
+
active = true;
|
|
1150
|
+
const lines = [
|
|
1151
|
+
"",
|
|
1152
|
+
formatGhostTunnel(context.ghostTunnel, {
|
|
1153
|
+
color: shouldColor(),
|
|
1154
|
+
label: "ready",
|
|
1155
|
+
verbose: true
|
|
1156
|
+
}) ?? "localghost ghost tunnel",
|
|
1157
|
+
""
|
|
1158
|
+
];
|
|
1159
|
+
if (concreteUrls.length === 0) {
|
|
1160
|
+
lines.push(" No concrete Ghost Tunnel domain configured.");
|
|
1161
|
+
lines.push(" Add ghostTunnel.domains to localghost.config.mjs to open a URL from this menu.");
|
|
1162
|
+
active = false;
|
|
1163
|
+
} else {
|
|
1164
|
+
concreteUrls.forEach((url, index) => {
|
|
1165
|
+
lines.push(` ${index + 1}. ${url}`);
|
|
1166
|
+
});
|
|
1167
|
+
lines.push("");
|
|
1168
|
+
lines.push(" Press a number to open, or escape to cancel.");
|
|
1169
|
+
}
|
|
1170
|
+
server.config.logger.info(lines.join("\n"), {
|
|
1171
|
+
clear: false,
|
|
1172
|
+
timestamp: false
|
|
1173
|
+
});
|
|
1174
|
+
};
|
|
1175
|
+
const onKeypress = (_input, key = {}) => {
|
|
1176
|
+
if (key.ctrl && key.name === "c") return;
|
|
1177
|
+
if (!active) {
|
|
1178
|
+
if (key.name === "g") printMenu();
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
if (key.name === "escape") {
|
|
1182
|
+
active = false;
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
const index = Number.parseInt(key.name ?? "", 10) - 1;
|
|
1186
|
+
const url = concreteUrls[index];
|
|
1187
|
+
if (!url) return;
|
|
1188
|
+
active = false;
|
|
1189
|
+
openExternalUrl(url);
|
|
1190
|
+
server.config.logger.info(`localghost opened ${url}`, {
|
|
1191
|
+
clear: false,
|
|
1192
|
+
timestamp: false
|
|
1193
|
+
});
|
|
1194
|
+
};
|
|
1195
|
+
process.stdin.on("keypress", onKeypress);
|
|
1196
|
+
return () => {
|
|
1197
|
+
process.stdin.off("keypress", onKeypress);
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
609
1200
|
function localGhostPlugin(options = {}) {
|
|
610
1201
|
let resolvedEntries = [];
|
|
611
1202
|
let resolvedVitePort;
|
|
612
1203
|
let resolvedHttps = false;
|
|
1204
|
+
let resolvedContext;
|
|
613
1205
|
let restartTimer;
|
|
1206
|
+
let activityRunId;
|
|
614
1207
|
return {
|
|
615
1208
|
name: "localghost:vite",
|
|
616
1209
|
enforce: "pre",
|
|
617
1210
|
async config(userConfig, configEnv) {
|
|
618
1211
|
if (configEnv.command !== "serve" || configEnv.mode === "production" || isProductionLike()) {
|
|
1212
|
+
await maybePrintBuildGhostTunnel(options);
|
|
619
1213
|
return {};
|
|
620
1214
|
}
|
|
621
1215
|
const existingServer = userConfig.server ?? {};
|
|
@@ -628,6 +1222,7 @@ function localGhostPlugin(options = {}) {
|
|
|
628
1222
|
resolvedEntries = entries;
|
|
629
1223
|
resolvedVitePort = context.port;
|
|
630
1224
|
resolvedHttps = context.https;
|
|
1225
|
+
resolvedContext = context;
|
|
631
1226
|
const server = {
|
|
632
1227
|
...existingServer,
|
|
633
1228
|
allowedHosts: mergeAllowedHosts(existingServer.allowedHosts, hosts),
|
|
@@ -687,9 +1282,38 @@ function localGhostPlugin(options = {}) {
|
|
|
687
1282
|
server.watcher.on("unlink", restartOnLocalghostConfigChange);
|
|
688
1283
|
if (options.log !== false) {
|
|
689
1284
|
server.printUrls = () => {
|
|
690
|
-
printLocalHosts(server,
|
|
1285
|
+
printLocalHosts(server, resolvedContext);
|
|
691
1286
|
};
|
|
692
1287
|
}
|
|
1288
|
+
if (resolvedContext) {
|
|
1289
|
+
const run = registerLocalghostRun({
|
|
1290
|
+
id: `${resolvedContext.projectName}:vite:${process.pid}:${resolvedContext.cwd}`,
|
|
1291
|
+
mode: "vite",
|
|
1292
|
+
pid: process.pid,
|
|
1293
|
+
cwd: resolvedContext.cwd,
|
|
1294
|
+
projectName: resolvedContext.projectName,
|
|
1295
|
+
configPath: resolvedContext.configPath,
|
|
1296
|
+
childCommand: ["vite"],
|
|
1297
|
+
https: resolvedContext.https,
|
|
1298
|
+
requestedPort: resolvedContext.requestedPort,
|
|
1299
|
+
port: resolvedContext.port,
|
|
1300
|
+
dynamicPort: resolvedContext.dynamicPort,
|
|
1301
|
+
entries: resolvedContext.entries
|
|
1302
|
+
});
|
|
1303
|
+
activityRunId = run.id;
|
|
1304
|
+
}
|
|
1305
|
+
const cleanupGhostMenu = installGhostTunnelMenu(server, resolvedContext);
|
|
1306
|
+
const cleanupActivity = () => {
|
|
1307
|
+
if (!activityRunId) return;
|
|
1308
|
+
unregisterLocalghostRun(activityRunId);
|
|
1309
|
+
activityRunId = void 0;
|
|
1310
|
+
};
|
|
1311
|
+
const cleanup = () => {
|
|
1312
|
+
cleanupActivity();
|
|
1313
|
+
cleanupGhostMenu?.();
|
|
1314
|
+
};
|
|
1315
|
+
server.httpServer?.once("close", cleanup);
|
|
1316
|
+
process.once("exit", cleanup);
|
|
693
1317
|
}
|
|
694
1318
|
};
|
|
695
1319
|
}
|