@hamedb89/localghost 0.1.8 → 0.1.9
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 +158 -27
- 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 +524 -63
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +130 -5
- package/dist/index.js +761 -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 +655 -36
- package/dist/vite.js.map +1 -1
- package/docs/flows.md +19 -0
- package/docs/ghost-tunnel.md +249 -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,257 @@ 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 createDisplayUrl(config, defaults, domain) {
|
|
408
|
+
const input2 = getPreviewDefaults(config.preview, defaults);
|
|
409
|
+
const protocol = input2.protocol ?? "https";
|
|
410
|
+
const slug = createNamespaceDisplaySlug(config.namespace, getDisplayValues(input2));
|
|
411
|
+
const entryHost = domain ? getGhostTunnelEntryHost(domain, config) : input2.domain ? getGhostTunnelEntryHost(input2.domain, config) : `${config.subdomain}.*`;
|
|
412
|
+
const url = `${protocol}://${slug}.${entryHost}/`;
|
|
413
|
+
if (!input2.path) return url;
|
|
414
|
+
return `${url}${input2.path.replace(/^\/+/, "")}`;
|
|
415
|
+
}
|
|
416
|
+
function createDisplayUrls(config, defaults) {
|
|
417
|
+
const domains = config.domains.length > 0 ? config.domains : defaults?.domain ? [defaults.domain] : [];
|
|
418
|
+
const urls = domains.length > 0 ? domains.map((domain) => createDisplayUrl(config, defaults, domain)) : [createDisplayUrl(config, defaults)];
|
|
419
|
+
return [...new Set(urls)];
|
|
420
|
+
}
|
|
421
|
+
function maybeConstructPreviewUrl(config, defaults) {
|
|
422
|
+
if (!config.preview) return void 0;
|
|
423
|
+
const input2 = getPreviewDefaults(config.preview, defaults);
|
|
424
|
+
if (!input2.domain || !input2.route || !input2.project || !input2.owner) return void 0;
|
|
425
|
+
return constructGhostTunnelUrl({
|
|
426
|
+
domain: input2.domain,
|
|
427
|
+
route: input2.route,
|
|
428
|
+
project: input2.project,
|
|
429
|
+
owner: input2.owner,
|
|
430
|
+
values: input2.values,
|
|
431
|
+
...input2.path ? { path: input2.path } : {},
|
|
432
|
+
...input2.protocol ? { protocol: input2.protocol } : {},
|
|
433
|
+
ghostTunnel: config
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
function resolveGhostTunnelConfig(options, defaults) {
|
|
437
|
+
if (options === false || typeof options === "undefined") {
|
|
438
|
+
return {
|
|
439
|
+
enabled: false,
|
|
440
|
+
mode: DEFAULT_GHOST_TUNNEL_MODE,
|
|
441
|
+
domains: [],
|
|
442
|
+
subdomain: DEFAULT_GHOST_TUNNEL_SUBDOMAIN,
|
|
443
|
+
namespace: resolveNamespaceConfig(void 0),
|
|
444
|
+
displayUrls: [],
|
|
445
|
+
requireHttps: true,
|
|
446
|
+
requireAuth: true
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
const config = typeof options === "string" ? { mode: options } : options;
|
|
450
|
+
const subdomain = config.subdomain ?? DEFAULT_GHOST_TUNNEL_SUBDOMAIN;
|
|
451
|
+
assertValidSubdomain(subdomain);
|
|
452
|
+
const domains = normalizeDomains(config.domains);
|
|
453
|
+
const enabled = config.enabled ?? true;
|
|
454
|
+
const resolved = {
|
|
455
|
+
enabled,
|
|
456
|
+
mode: parseGhostTunnelMode(config.mode),
|
|
457
|
+
domains,
|
|
458
|
+
subdomain,
|
|
459
|
+
namespace: resolveNamespaceConfig(config.namespace),
|
|
460
|
+
...config.preview ? { preview: config.preview } : {},
|
|
461
|
+
displayUrls: [],
|
|
462
|
+
requireHttps: config.requireHttps ?? true,
|
|
463
|
+
requireAuth: config.requireAuth ?? true
|
|
464
|
+
};
|
|
465
|
+
if (!enabled) {
|
|
466
|
+
return resolved;
|
|
467
|
+
}
|
|
468
|
+
const previewUrl = maybeConstructPreviewUrl(resolved, defaults);
|
|
469
|
+
const displayUrls = previewUrl ? [previewUrl] : createDisplayUrls(resolved, defaults);
|
|
470
|
+
return {
|
|
471
|
+
...resolved,
|
|
472
|
+
displayUrls,
|
|
473
|
+
...displayUrls[0] ? { displayUrl: displayUrls[0] } : {},
|
|
474
|
+
...previewUrl ? { previewUrl } : {}
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function getGhostTunnelEntryHost(domain, options = {}) {
|
|
478
|
+
const config = toGhostTunnelConfig(options);
|
|
479
|
+
const normalizedDomain = normalizeDomain(domain);
|
|
480
|
+
if (!normalizedDomain) {
|
|
481
|
+
throw new Error(`Invalid ghost tunnel domain: ${domain}`);
|
|
482
|
+
}
|
|
483
|
+
return `${config.subdomain}.${normalizedDomain}`;
|
|
484
|
+
}
|
|
485
|
+
function constructGhostTunnelHost(input2) {
|
|
486
|
+
const config = toGhostTunnelConfig(input2.ghostTunnel ?? {});
|
|
487
|
+
if (!config.enabled) {
|
|
488
|
+
throw new Error("Ghost tunnel is not enabled.");
|
|
489
|
+
}
|
|
490
|
+
const namespaceValues = {
|
|
491
|
+
route: input2.route,
|
|
492
|
+
project: input2.project,
|
|
493
|
+
owner: input2.owner,
|
|
494
|
+
...input2.values ?? {}
|
|
495
|
+
};
|
|
496
|
+
const slug = createNamespaceSlug(config.namespace, namespaceValues);
|
|
497
|
+
return `${slug}.${getGhostTunnelEntryHost(input2.domain, config)}`;
|
|
498
|
+
}
|
|
499
|
+
function constructGhostTunnelUrl(input2) {
|
|
500
|
+
const protocol = input2.protocol ?? "https";
|
|
501
|
+
const host = constructGhostTunnelHost(input2);
|
|
502
|
+
const url = new URL(`${protocol}://${host}/`);
|
|
503
|
+
if (input2.path) {
|
|
504
|
+
url.pathname = `/${input2.path.replace(/^\/+/, "")}`;
|
|
505
|
+
}
|
|
506
|
+
if (input2.searchParams instanceof URLSearchParams) {
|
|
507
|
+
url.search = input2.searchParams.toString();
|
|
508
|
+
} else if (input2.searchParams) {
|
|
509
|
+
for (const [key, value] of Object.entries(input2.searchParams)) {
|
|
510
|
+
if (typeof value !== "undefined" && value !== null) {
|
|
511
|
+
url.searchParams.set(key, String(value));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return url.toString();
|
|
516
|
+
}
|
|
517
|
+
|
|
151
518
|
// src/context.ts
|
|
152
519
|
var LOCALGHOST_PROJECT_CONFIG_FILES = [
|
|
153
520
|
"localghost.config.mjs",
|
|
@@ -172,6 +539,25 @@ function envHttps() {
|
|
|
172
539
|
if (!value) return void 0;
|
|
173
540
|
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
174
541
|
}
|
|
542
|
+
function getPackageName(cwd) {
|
|
543
|
+
try {
|
|
544
|
+
const pkg = JSON.parse(readFileSync3(join3(cwd, "package.json"), "utf8"));
|
|
545
|
+
return typeof pkg.name === "string" ? pkg.name : void 0;
|
|
546
|
+
} catch {
|
|
547
|
+
return void 0;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
function getPackageOwner(cwd) {
|
|
551
|
+
const packageName = getPackageName(cwd);
|
|
552
|
+
if (!packageName?.startsWith("@")) return void 0;
|
|
553
|
+
return packageName.slice(1).split("/")[0];
|
|
554
|
+
}
|
|
555
|
+
function getLocalOwner(cwd) {
|
|
556
|
+
return sanitizeProjectName(process.env.LOCALGHOST_OWNER ?? getPackageOwner(cwd) ?? process.env.USER ?? process.env.USERNAME ?? "local");
|
|
557
|
+
}
|
|
558
|
+
function getRouteName(primaryHost, fallback) {
|
|
559
|
+
return sanitizeProjectName(primaryHost.split(".")[0] ?? fallback);
|
|
560
|
+
}
|
|
175
561
|
function readOptionsFromContext(options) {
|
|
176
562
|
return {
|
|
177
563
|
cwd: options.cwd ?? process.cwd(),
|
|
@@ -210,18 +596,22 @@ function addDefaultWwwAliases(entries) {
|
|
|
210
596
|
function defined(input2) {
|
|
211
597
|
return Object.fromEntries(Object.entries(input2).filter(([, value]) => typeof value !== "undefined"));
|
|
212
598
|
}
|
|
213
|
-
async function
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
599
|
+
async function readLocalghostProjectConfig(options = {}) {
|
|
600
|
+
const cwd = options.cwd ?? process.cwd();
|
|
601
|
+
if (options.configFile === false) return { config: {} };
|
|
602
|
+
const candidates = options.configFile ? [options.configFile] : LOCALGHOST_PROJECT_CONFIG_FILES;
|
|
603
|
+
const path = candidates.map((candidate) => resolveDevHostsPath({ cwd, fileName: candidate }).path).find((candidate) => existsSync3(candidate));
|
|
604
|
+
if (!path) return { config: {} };
|
|
218
605
|
const imported = await import(`${pathToFileURL(path).href}?localghost=${Date.now()}`);
|
|
219
606
|
const config = imported.default ?? imported;
|
|
220
607
|
return { config, path };
|
|
221
608
|
}
|
|
222
609
|
async function resolveLocalghostContext(options = {}) {
|
|
223
610
|
const cwd = options.cwd ?? process.cwd();
|
|
224
|
-
const projectConfig = await
|
|
611
|
+
const projectConfig = await readLocalghostProjectConfig({
|
|
612
|
+
cwd,
|
|
613
|
+
...typeof options.localghostConfig !== "undefined" ? { configFile: options.localghostConfig } : {}
|
|
614
|
+
});
|
|
225
615
|
const merged = {
|
|
226
616
|
...projectConfig.config,
|
|
227
617
|
...defined(options)
|
|
@@ -230,7 +620,7 @@ async function resolveLocalghostContext(options = {}) {
|
|
|
230
620
|
const resolvedPath = resolveDevHostsPath(readOptions);
|
|
231
621
|
const configEntries = readDevHosts(readOptions);
|
|
232
622
|
const requestedPort = merged.port ?? envPort() ?? configEntries[0]?.port ?? 5173;
|
|
233
|
-
const dynamicPort = merged.dynamicPort ?? envDynamicPort() ??
|
|
623
|
+
const dynamicPort = merged.dynamicPort ?? envDynamicPort() ?? true;
|
|
234
624
|
const bindHost = merged.bindHost ?? "127.0.0.1";
|
|
235
625
|
const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
|
|
236
626
|
const port = dynamicPort ? await findAvailablePort(requestedPort, { host: probeHost }) : requestedPort;
|
|
@@ -238,9 +628,15 @@ async function resolveLocalghostContext(options = {}) {
|
|
|
238
628
|
const entries = wwwAlias ? addDefaultWwwAliases(withRuntimePort(configEntries, requestedPort, port)) : withRuntimePort(configEntries, requestedPort, port);
|
|
239
629
|
const hosts = uniqueHosts(entries);
|
|
240
630
|
const primaryHost = merged.primaryHost ?? entries.find((entry) => entry.port === port)?.host ?? hosts[0] ?? `${sanitizeProjectName(getProjectName(cwd))}.localhost`;
|
|
631
|
+
const projectName = sanitizeProjectName(merged.project ?? getProjectName(cwd));
|
|
632
|
+
const ghostTunnel = resolveGhostTunnelConfig(merged.ghostTunnel, {
|
|
633
|
+
route: getRouteName(primaryHost, projectName),
|
|
634
|
+
project: projectName,
|
|
635
|
+
owner: getLocalOwner(cwd)
|
|
636
|
+
});
|
|
241
637
|
return {
|
|
242
638
|
cwd,
|
|
243
|
-
projectName
|
|
639
|
+
projectName,
|
|
244
640
|
readOptions,
|
|
245
641
|
configPath: resolvedPath.path,
|
|
246
642
|
configFileName: resolvedPath.fileName,
|
|
@@ -254,6 +650,7 @@ async function resolveLocalghostContext(options = {}) {
|
|
|
254
650
|
primaryHost,
|
|
255
651
|
https: merged.https ?? envHttps() ?? false,
|
|
256
652
|
wwwAlias,
|
|
653
|
+
ghostTunnel,
|
|
257
654
|
...projectConfig.path ? { projectConfigPath: projectConfig.path } : {}
|
|
258
655
|
};
|
|
259
656
|
}
|
|
@@ -293,21 +690,21 @@ function isProductionLike(env = process.env) {
|
|
|
293
690
|
}
|
|
294
691
|
|
|
295
692
|
// src/fs.ts
|
|
296
|
-
import { mkdirSync, readFileSync as
|
|
297
|
-
import { dirname } from "path";
|
|
693
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
694
|
+
import { dirname as dirname2 } from "path";
|
|
298
695
|
function readTextFile(path) {
|
|
299
|
-
return
|
|
696
|
+
return readFileSync4(path, "utf8");
|
|
300
697
|
}
|
|
301
698
|
function writeTextFile(path, value) {
|
|
302
|
-
|
|
303
|
-
|
|
699
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
700
|
+
writeFileSync2(path, value, "utf8");
|
|
304
701
|
return path;
|
|
305
702
|
}
|
|
306
703
|
|
|
307
704
|
// src/hosts-file.ts
|
|
308
|
-
import { writeFileSync as
|
|
705
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
309
706
|
import { tmpdir } from "os";
|
|
310
|
-
import { join as
|
|
707
|
+
import { join as join4 } from "path";
|
|
311
708
|
import { execa as execa2 } from "execa";
|
|
312
709
|
function escapeRegExp(value) {
|
|
313
710
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -342,8 +739,8 @@ ${block}`;
|
|
|
342
739
|
}
|
|
343
740
|
async function writeSystemHostsFile(hostsPath, next, projectName) {
|
|
344
741
|
const sanitizedProjectName = sanitizeProjectName(projectName);
|
|
345
|
-
const tempPath =
|
|
346
|
-
|
|
742
|
+
const tempPath = join4(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
|
|
743
|
+
writeFileSync3(tempPath, next, "utf8");
|
|
347
744
|
if (process.platform === "win32") {
|
|
348
745
|
throw new Error(`Windows support: run as administrator and copy ${tempPath} to ${hostsPath}.`);
|
|
349
746
|
}
|
|
@@ -394,15 +791,15 @@ async function ask(question, defaultValue) {
|
|
|
394
791
|
}
|
|
395
792
|
|
|
396
793
|
// src/state.ts
|
|
397
|
-
import { existsSync as
|
|
398
|
-
import { join as
|
|
794
|
+
import { existsSync as existsSync4 } from "fs";
|
|
795
|
+
import { join as join5 } from "path";
|
|
399
796
|
var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
|
|
400
797
|
function getLocalghostStatePath(cwd = process.cwd()) {
|
|
401
|
-
return
|
|
798
|
+
return join5(cwd, LOCALGHOST_STATE_FILE);
|
|
402
799
|
}
|
|
403
800
|
function readLocalghostState(cwd = process.cwd()) {
|
|
404
801
|
const path = getLocalghostStatePath(cwd);
|
|
405
|
-
if (!
|
|
802
|
+
if (!existsSync4(path)) return null;
|
|
406
803
|
return JSON.parse(readTextFile(path));
|
|
407
804
|
}
|
|
408
805
|
function writeLocalghostState(cwd, state) {
|
|
@@ -413,7 +810,7 @@ function writeLocalghostState(cwd, state) {
|
|
|
413
810
|
}
|
|
414
811
|
|
|
415
812
|
// src/caddy.ts
|
|
416
|
-
import { dirname as
|
|
813
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
417
814
|
import { execa as execa3 } from "execa";
|
|
418
815
|
function groupByPort(entries) {
|
|
419
816
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -425,7 +822,7 @@ function groupByPort(entries) {
|
|
|
425
822
|
return groups;
|
|
426
823
|
}
|
|
427
824
|
function getCaddyfilePath(cwd = process.cwd()) {
|
|
428
|
-
return
|
|
825
|
+
return join6(cwd, "ops/local/Caddyfile");
|
|
429
826
|
}
|
|
430
827
|
function renderCaddyfile(entries, options = {}) {
|
|
431
828
|
const groups = groupByPort(entries);
|
|
@@ -451,11 +848,54 @@ async function writeCaddyfile(entries, cwd = process.cwd(), options = {}) {
|
|
|
451
848
|
}
|
|
452
849
|
async function validateCaddyfile(path) {
|
|
453
850
|
await execa3("caddy", ["validate", "--config", path], {
|
|
454
|
-
cwd:
|
|
851
|
+
cwd: dirname3(path),
|
|
455
852
|
stdio: "inherit"
|
|
456
853
|
});
|
|
457
854
|
}
|
|
458
855
|
|
|
856
|
+
// src/routes.ts
|
|
857
|
+
var ansi = {
|
|
858
|
+
cyan: "\x1B[36m",
|
|
859
|
+
dim: "\x1B[2m",
|
|
860
|
+
green: "\x1B[32m",
|
|
861
|
+
reset: "\x1B[0m",
|
|
862
|
+
yellow: "\x1B[33m"
|
|
863
|
+
};
|
|
864
|
+
function colorize(value, color, enabled) {
|
|
865
|
+
return enabled ? `${color}${value}${ansi.reset}` : value;
|
|
866
|
+
}
|
|
867
|
+
function colorizeUrl(value, enabled) {
|
|
868
|
+
if (!enabled) return value;
|
|
869
|
+
return colorize(value.replace(/\*/g, `${ansi.yellow}*${ansi.cyan}`), ansi.cyan, enabled);
|
|
870
|
+
}
|
|
871
|
+
function formatGhostTunnel(config, options = {}) {
|
|
872
|
+
if (!config.enabled) return null;
|
|
873
|
+
const color = options.color === true;
|
|
874
|
+
const label = options.label ?? "expected";
|
|
875
|
+
const labelColor = label === "running" ? ansi.green : ansi.dim;
|
|
876
|
+
const lines = [
|
|
877
|
+
"localghost ghost tunnel",
|
|
878
|
+
` mode: ${config.mode}`
|
|
879
|
+
];
|
|
880
|
+
const urls = config.displayUrls.length > 0 ? config.displayUrls : config.displayUrl ? [config.displayUrl] : [];
|
|
881
|
+
if (urls.length === 0) {
|
|
882
|
+
lines.push(` ${label}: unavailable`);
|
|
883
|
+
} else if (urls.length === 1) {
|
|
884
|
+
lines.push(` ${colorize(label, labelColor, color)}: ${colorizeUrl(urls[0], color)}`);
|
|
885
|
+
} else {
|
|
886
|
+
lines.push(` ${colorize(label, labelColor, color)}:`);
|
|
887
|
+
for (const url of urls) {
|
|
888
|
+
lines.push(` ${colorizeUrl(url, color)}`);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
if (options.verbose) {
|
|
892
|
+
lines.push(` domains: ${config.domains.length > 0 ? config.domains.join(", ") : "*"}`);
|
|
893
|
+
lines.push(` access: ${config.requireAuth ? "auth required" : "app decides"}`);
|
|
894
|
+
lines.push(` transport: ${config.requireHttps ? "https required" : "http allowed"}`);
|
|
895
|
+
}
|
|
896
|
+
return lines.join("\n");
|
|
897
|
+
}
|
|
898
|
+
|
|
459
899
|
// src/vite.ts
|
|
460
900
|
function mergeAllowedHosts(current, hosts) {
|
|
461
901
|
if (Array.isArray(current)) {
|
|
@@ -470,7 +910,11 @@ function getDisplayEntries(entries, vitePort) {
|
|
|
470
910
|
const matchingEntries = entries.filter((entry) => entry.port === vitePort);
|
|
471
911
|
return matchingEntries.length > 0 ? matchingEntries : entries;
|
|
472
912
|
}
|
|
473
|
-
function printLocalHosts(server,
|
|
913
|
+
function printLocalHosts(server, context) {
|
|
914
|
+
if (!context) return;
|
|
915
|
+
const entries = context.entries;
|
|
916
|
+
const vitePort = context.port;
|
|
917
|
+
const https = context.https;
|
|
474
918
|
const displayEntries = getDisplayEntries(entries, vitePort);
|
|
475
919
|
const protocol = https ? "https" : "http";
|
|
476
920
|
const urls = displayEntries.map((entry) => `${protocol}://${entry.host}/`);
|
|
@@ -484,13 +928,24 @@ function printLocalHosts(server, entries, vitePort, https) {
|
|
|
484
928
|
` local: ${primaryUrl}`,
|
|
485
929
|
...urls.slice(1).map((url) => ` also: ${url}`),
|
|
486
930
|
vitePort ? ` target: http://127.0.0.1:${vitePort}/` : void 0,
|
|
487
|
-
https ? " proxy: Caddy local HTTPS" : void 0
|
|
931
|
+
https ? " proxy: Caddy local HTTPS" : void 0,
|
|
932
|
+
context.ghostTunnel.enabled ? formatGhostTunnel(context.ghostTunnel, {
|
|
933
|
+
color: shouldColor(),
|
|
934
|
+
label: "ready",
|
|
935
|
+
verbose: optionsVerbose(context)
|
|
936
|
+
}) : void 0
|
|
488
937
|
].filter((line) => Boolean(line));
|
|
489
938
|
server.config.logger.info(lines.join("\n"), {
|
|
490
939
|
clear: false,
|
|
491
940
|
timestamp: false
|
|
492
941
|
});
|
|
493
942
|
}
|
|
943
|
+
function shouldColor() {
|
|
944
|
+
return process.stdout.isTTY && !process.env.NO_COLOR;
|
|
945
|
+
}
|
|
946
|
+
function optionsVerbose(context) {
|
|
947
|
+
return process.env.LOCALGHOST_VERBOSE === "1" || process.env.LOCALGHOST_VERBOSE === "true" || context.ghostTunnel.displayUrls.length > 1;
|
|
948
|
+
}
|
|
494
949
|
function readOptionsFromPlugin(options) {
|
|
495
950
|
return {
|
|
496
951
|
cwd: options.cwd ?? process.cwd(),
|
|
@@ -522,6 +977,57 @@ function defaultHost(cwd) {
|
|
|
522
977
|
const projectName = sanitizeProjectName(getProjectName(cwd).split("/").pop() ?? "app");
|
|
523
978
|
return `${projectName}.localhost`;
|
|
524
979
|
}
|
|
980
|
+
function getPackageOwner2(cwd) {
|
|
981
|
+
try {
|
|
982
|
+
const pkg = JSON.parse(readFileSync5(resolve2(cwd, "package.json"), "utf8"));
|
|
983
|
+
if (typeof pkg.name === "string" && pkg.name.startsWith("@")) {
|
|
984
|
+
return pkg.name.slice(1).split("/")[0];
|
|
985
|
+
}
|
|
986
|
+
} catch {
|
|
987
|
+
return void 0;
|
|
988
|
+
}
|
|
989
|
+
return void 0;
|
|
990
|
+
}
|
|
991
|
+
function getLocalOwner2(cwd) {
|
|
992
|
+
return sanitizeProjectName(process.env.LOCALGHOST_OWNER ?? getPackageOwner2(cwd) ?? process.env.USER ?? process.env.USERNAME ?? "local");
|
|
993
|
+
}
|
|
994
|
+
function getRouteName2(primaryHost, fallback) {
|
|
995
|
+
return sanitizeProjectName(primaryHost.split(".")[0] ?? fallback);
|
|
996
|
+
}
|
|
997
|
+
function readBuildEntries(options) {
|
|
998
|
+
const readOptions = readOptionsFromPlugin(options);
|
|
999
|
+
const resolved = resolveDevHostsPath(readOptions);
|
|
1000
|
+
if (!resolved.exists) return [];
|
|
1001
|
+
try {
|
|
1002
|
+
return readDevHosts(readOptions);
|
|
1003
|
+
} catch {
|
|
1004
|
+
return [];
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
async function maybePrintBuildGhostTunnel(options) {
|
|
1008
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1009
|
+
const projectConfig = await readLocalghostProjectConfig({
|
|
1010
|
+
cwd,
|
|
1011
|
+
...typeof options.localghostConfig !== "undefined" ? { configFile: options.localghostConfig } : {}
|
|
1012
|
+
});
|
|
1013
|
+
const explicitGhostTunnel = typeof options.ghostTunnel !== "undefined" ? options.ghostTunnel : projectConfig.config.ghostTunnel;
|
|
1014
|
+
if (!explicitGhostTunnel) return;
|
|
1015
|
+
const entries = readBuildEntries(options);
|
|
1016
|
+
const projectName = sanitizeProjectName(projectConfig.config.project ?? getProjectName(cwd));
|
|
1017
|
+
const primaryHost = options.primaryHost ?? entries[0]?.host ?? defaultHost(cwd);
|
|
1018
|
+
const ghostTunnel = resolveGhostTunnelConfig(explicitGhostTunnel, {
|
|
1019
|
+
route: getRouteName2(primaryHost, projectName),
|
|
1020
|
+
project: sanitizeProjectName(projectConfig.config.project ?? getProjectName(cwd)),
|
|
1021
|
+
owner: getLocalOwner2(cwd)
|
|
1022
|
+
});
|
|
1023
|
+
if (!ghostTunnel.enabled) return;
|
|
1024
|
+
const formatted = formatGhostTunnel(ghostTunnel, {
|
|
1025
|
+
color: shouldColor(),
|
|
1026
|
+
label: "configured",
|
|
1027
|
+
verbose: options.verbose === true || process.env.LOCALGHOST_VERBOSE === "1" || process.env.LOCALGHOST_VERBOSE === "true"
|
|
1028
|
+
});
|
|
1029
|
+
if (formatted) console.log(formatted);
|
|
1030
|
+
}
|
|
525
1031
|
async function promptForHosts(cwd, port) {
|
|
526
1032
|
const primaryHost = await ask("Primary local domain", defaultHost(cwd));
|
|
527
1033
|
const hosts = [primaryHost.toLowerCase()];
|
|
@@ -536,13 +1042,13 @@ function hasReadySetup(cwd, entries, configPath, https) {
|
|
|
536
1042
|
const projectName = sanitizeProjectName(getProjectName(cwd));
|
|
537
1043
|
if (state?.action !== "setup" || state.configPath !== configPath) return false;
|
|
538
1044
|
try {
|
|
539
|
-
const hosts =
|
|
1045
|
+
const hosts = readFileSync5(getSystemHostsPath(), "utf8");
|
|
540
1046
|
if (!hosts.includes(renderHostsBlock(projectName, entries).trimEnd())) return false;
|
|
541
1047
|
} catch {
|
|
542
1048
|
return false;
|
|
543
1049
|
}
|
|
544
1050
|
const caddyfilePath = getCaddyfilePath(cwd);
|
|
545
|
-
return
|
|
1051
|
+
return existsSync5(caddyfilePath) && readFileSync5(caddyfilePath, "utf8") === renderCaddyfile(entries, { https });
|
|
546
1052
|
}
|
|
547
1053
|
async function setupProject(cwd, entries, configPath, https) {
|
|
548
1054
|
const caddy = await checkCaddy();
|
|
@@ -571,6 +1077,14 @@ async function setupProject(cwd, entries, configPath, https) {
|
|
|
571
1077
|
caddyHttps: https,
|
|
572
1078
|
entries
|
|
573
1079
|
});
|
|
1080
|
+
registerLocalghostSetup({
|
|
1081
|
+
cwd,
|
|
1082
|
+
projectName,
|
|
1083
|
+
configPath,
|
|
1084
|
+
caddyfilePath,
|
|
1085
|
+
https,
|
|
1086
|
+
entries
|
|
1087
|
+
});
|
|
574
1088
|
}
|
|
575
1089
|
async function ensureLocalghostContext(options, vitePort, https) {
|
|
576
1090
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -606,16 +1120,91 @@ async function ensureLocalghostContext(options, vitePort, https) {
|
|
|
606
1120
|
}
|
|
607
1121
|
return context;
|
|
608
1122
|
}
|
|
1123
|
+
function isConcreteGhostUrl(url) {
|
|
1124
|
+
return !url.includes("*") && !url.includes("<") && /^https?:\/\//.test(url);
|
|
1125
|
+
}
|
|
1126
|
+
function getConcreteGhostUrls(context) {
|
|
1127
|
+
return context.ghostTunnel.displayUrls.filter(isConcreteGhostUrl);
|
|
1128
|
+
}
|
|
1129
|
+
function openExternalUrl(url) {
|
|
1130
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
1131
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
1132
|
+
const child = spawn(command, args, {
|
|
1133
|
+
detached: true,
|
|
1134
|
+
stdio: "ignore"
|
|
1135
|
+
});
|
|
1136
|
+
child.unref();
|
|
1137
|
+
}
|
|
1138
|
+
function installGhostTunnelMenu(server, context) {
|
|
1139
|
+
if (!context?.ghostTunnel.enabled || !process.stdin.isTTY) return void 0;
|
|
1140
|
+
emitKeypressEvents(process.stdin);
|
|
1141
|
+
let active = false;
|
|
1142
|
+
const concreteUrls = getConcreteGhostUrls(context);
|
|
1143
|
+
const printMenu = () => {
|
|
1144
|
+
active = true;
|
|
1145
|
+
const lines = [
|
|
1146
|
+
"",
|
|
1147
|
+
formatGhostTunnel(context.ghostTunnel, {
|
|
1148
|
+
color: shouldColor(),
|
|
1149
|
+
label: "ready",
|
|
1150
|
+
verbose: true
|
|
1151
|
+
}) ?? "localghost ghost tunnel",
|
|
1152
|
+
""
|
|
1153
|
+
];
|
|
1154
|
+
if (concreteUrls.length === 0) {
|
|
1155
|
+
lines.push(" No concrete Ghost Tunnel domain configured.");
|
|
1156
|
+
lines.push(" Add ghostTunnel.domains to localghost.config.mjs to open a URL from this menu.");
|
|
1157
|
+
active = false;
|
|
1158
|
+
} else {
|
|
1159
|
+
concreteUrls.forEach((url, index) => {
|
|
1160
|
+
lines.push(` ${index + 1}. ${url}`);
|
|
1161
|
+
});
|
|
1162
|
+
lines.push("");
|
|
1163
|
+
lines.push(" Press a number to open, or escape to cancel.");
|
|
1164
|
+
}
|
|
1165
|
+
server.config.logger.info(lines.join("\n"), {
|
|
1166
|
+
clear: false,
|
|
1167
|
+
timestamp: false
|
|
1168
|
+
});
|
|
1169
|
+
};
|
|
1170
|
+
const onKeypress = (_input, key = {}) => {
|
|
1171
|
+
if (key.ctrl && key.name === "c") return;
|
|
1172
|
+
if (!active) {
|
|
1173
|
+
if (key.name === "g") printMenu();
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
if (key.name === "escape") {
|
|
1177
|
+
active = false;
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
const index = Number.parseInt(key.name ?? "", 10) - 1;
|
|
1181
|
+
const url = concreteUrls[index];
|
|
1182
|
+
if (!url) return;
|
|
1183
|
+
active = false;
|
|
1184
|
+
openExternalUrl(url);
|
|
1185
|
+
server.config.logger.info(`localghost opened ${url}`, {
|
|
1186
|
+
clear: false,
|
|
1187
|
+
timestamp: false
|
|
1188
|
+
});
|
|
1189
|
+
};
|
|
1190
|
+
process.stdin.on("keypress", onKeypress);
|
|
1191
|
+
return () => {
|
|
1192
|
+
process.stdin.off("keypress", onKeypress);
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
609
1195
|
function localGhostPlugin(options = {}) {
|
|
610
1196
|
let resolvedEntries = [];
|
|
611
1197
|
let resolvedVitePort;
|
|
612
1198
|
let resolvedHttps = false;
|
|
1199
|
+
let resolvedContext;
|
|
613
1200
|
let restartTimer;
|
|
1201
|
+
let activityRunId;
|
|
614
1202
|
return {
|
|
615
1203
|
name: "localghost:vite",
|
|
616
1204
|
enforce: "pre",
|
|
617
1205
|
async config(userConfig, configEnv) {
|
|
618
1206
|
if (configEnv.command !== "serve" || configEnv.mode === "production" || isProductionLike()) {
|
|
1207
|
+
await maybePrintBuildGhostTunnel(options);
|
|
619
1208
|
return {};
|
|
620
1209
|
}
|
|
621
1210
|
const existingServer = userConfig.server ?? {};
|
|
@@ -628,6 +1217,7 @@ function localGhostPlugin(options = {}) {
|
|
|
628
1217
|
resolvedEntries = entries;
|
|
629
1218
|
resolvedVitePort = context.port;
|
|
630
1219
|
resolvedHttps = context.https;
|
|
1220
|
+
resolvedContext = context;
|
|
631
1221
|
const server = {
|
|
632
1222
|
...existingServer,
|
|
633
1223
|
allowedHosts: mergeAllowedHosts(existingServer.allowedHosts, hosts),
|
|
@@ -687,9 +1277,38 @@ function localGhostPlugin(options = {}) {
|
|
|
687
1277
|
server.watcher.on("unlink", restartOnLocalghostConfigChange);
|
|
688
1278
|
if (options.log !== false) {
|
|
689
1279
|
server.printUrls = () => {
|
|
690
|
-
printLocalHosts(server,
|
|
1280
|
+
printLocalHosts(server, resolvedContext);
|
|
691
1281
|
};
|
|
692
1282
|
}
|
|
1283
|
+
if (resolvedContext) {
|
|
1284
|
+
const run = registerLocalghostRun({
|
|
1285
|
+
id: `${resolvedContext.projectName}:vite:${process.pid}:${resolvedContext.cwd}`,
|
|
1286
|
+
mode: "vite",
|
|
1287
|
+
pid: process.pid,
|
|
1288
|
+
cwd: resolvedContext.cwd,
|
|
1289
|
+
projectName: resolvedContext.projectName,
|
|
1290
|
+
configPath: resolvedContext.configPath,
|
|
1291
|
+
childCommand: ["vite"],
|
|
1292
|
+
https: resolvedContext.https,
|
|
1293
|
+
requestedPort: resolvedContext.requestedPort,
|
|
1294
|
+
port: resolvedContext.port,
|
|
1295
|
+
dynamicPort: resolvedContext.dynamicPort,
|
|
1296
|
+
entries: resolvedContext.entries
|
|
1297
|
+
});
|
|
1298
|
+
activityRunId = run.id;
|
|
1299
|
+
}
|
|
1300
|
+
const cleanupGhostMenu = installGhostTunnelMenu(server, resolvedContext);
|
|
1301
|
+
const cleanupActivity = () => {
|
|
1302
|
+
if (!activityRunId) return;
|
|
1303
|
+
unregisterLocalghostRun(activityRunId);
|
|
1304
|
+
activityRunId = void 0;
|
|
1305
|
+
};
|
|
1306
|
+
const cleanup = () => {
|
|
1307
|
+
cleanupActivity();
|
|
1308
|
+
cleanupGhostMenu?.();
|
|
1309
|
+
};
|
|
1310
|
+
server.httpServer?.once("close", cleanup);
|
|
1311
|
+
process.once("exit", cleanup);
|
|
693
1312
|
}
|
|
694
1313
|
};
|
|
695
1314
|
}
|