@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/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { existsSync as existsSync7, readFileSync as
|
|
4
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, unlinkSync } from "fs";
|
|
5
5
|
import { Command, InvalidArgumentError } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/activity.ts
|
|
@@ -25,7 +25,7 @@ function isProcessRunning(pid) {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
function emptyActivity() {
|
|
28
|
-
return { version: LOCALGHOST_ACTIVITY_VERSION, runs: [] };
|
|
28
|
+
return { version: LOCALGHOST_ACTIVITY_VERSION, runs: [], setups: [] };
|
|
29
29
|
}
|
|
30
30
|
function readLocalghostActivity(path = getLocalghostActivityPath()) {
|
|
31
31
|
if (!existsSync(path)) return emptyActivity();
|
|
@@ -33,7 +33,8 @@ function readLocalghostActivity(path = getLocalghostActivityPath()) {
|
|
|
33
33
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
34
34
|
return {
|
|
35
35
|
version: LOCALGHOST_ACTIVITY_VERSION,
|
|
36
|
-
runs: Array.isArray(parsed.runs) ? parsed.runs : []
|
|
36
|
+
runs: Array.isArray(parsed.runs) ? parsed.runs : [],
|
|
37
|
+
setups: Array.isArray(parsed.setups) ? parsed.setups : []
|
|
37
38
|
};
|
|
38
39
|
} catch {
|
|
39
40
|
return emptyActivity();
|
|
@@ -48,22 +49,29 @@ function writeLocalghostActivity(activity, path = getLocalghostActivityPath()) {
|
|
|
48
49
|
function createRunId(input2, pid) {
|
|
49
50
|
return `${input2.projectName}:${input2.mode}:${pid}:${Date.now()}`;
|
|
50
51
|
}
|
|
52
|
+
function createSetupId(input2) {
|
|
53
|
+
return `${input2.projectName}:${input2.cwd}:${input2.configPath ?? ""}`;
|
|
54
|
+
}
|
|
51
55
|
function pruneLocalghostActivity(path = getLocalghostActivityPath()) {
|
|
52
56
|
const activity = readLocalghostActivity(path);
|
|
53
57
|
const activeRuns = activity.runs.filter((run) => isProcessRunning(run.pid));
|
|
54
58
|
const pruned = activeRuns.length !== activity.runs.length;
|
|
55
59
|
if (pruned) {
|
|
56
|
-
writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs: activeRuns }, path);
|
|
60
|
+
writeLocalghostActivity({ ...activity, version: LOCALGHOST_ACTIVITY_VERSION, runs: activeRuns }, path);
|
|
57
61
|
}
|
|
58
62
|
return {
|
|
59
63
|
path,
|
|
60
64
|
pruned,
|
|
61
|
-
runs: activeRuns
|
|
65
|
+
runs: activeRuns,
|
|
66
|
+
setups: activity.setups
|
|
62
67
|
};
|
|
63
68
|
}
|
|
64
69
|
function listLocalghostRuns(path = getLocalghostActivityPath()) {
|
|
65
70
|
return pruneLocalghostActivity(path).runs;
|
|
66
71
|
}
|
|
72
|
+
function listLocalghostSetups(path = getLocalghostActivityPath()) {
|
|
73
|
+
return pruneLocalghostActivity(path).setups;
|
|
74
|
+
}
|
|
67
75
|
function registerLocalghostRun(input2, path = getLocalghostActivityPath()) {
|
|
68
76
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
69
77
|
const pid = input2.pid ?? process.pid;
|
|
@@ -87,14 +95,43 @@ function registerLocalghostRun(input2, path = getLocalghostActivityPath()) {
|
|
|
87
95
|
entries: input2.entries
|
|
88
96
|
};
|
|
89
97
|
const current = pruneLocalghostActivity(path).runs.filter((run) => run.id !== record.id);
|
|
90
|
-
|
|
98
|
+
const activity = readLocalghostActivity(path);
|
|
99
|
+
writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs: [...current, record], setups: activity.setups }, path);
|
|
100
|
+
return record;
|
|
101
|
+
}
|
|
102
|
+
function registerLocalghostSetup(input2, path = getLocalghostActivityPath()) {
|
|
103
|
+
const record = {
|
|
104
|
+
id: input2.id ?? createSetupId(input2),
|
|
105
|
+
cwd: input2.cwd,
|
|
106
|
+
projectName: input2.projectName,
|
|
107
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
108
|
+
...input2.configPath ? { configPath: input2.configPath } : {},
|
|
109
|
+
...input2.caddyfilePath ? { caddyfilePath: input2.caddyfilePath } : {},
|
|
110
|
+
...typeof input2.https === "boolean" ? { https: input2.https } : {},
|
|
111
|
+
entries: input2.entries
|
|
112
|
+
};
|
|
113
|
+
const activity = pruneLocalghostActivity(path);
|
|
114
|
+
const setups = activity.setups.filter((setup) => setup.id !== record.id);
|
|
115
|
+
writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs: activity.runs, setups: [...setups, record] }, path);
|
|
91
116
|
return record;
|
|
92
117
|
}
|
|
93
118
|
function unregisterLocalghostRun(id, path = getLocalghostActivityPath()) {
|
|
94
119
|
const activity = readLocalghostActivity(path);
|
|
95
120
|
const runs = activity.runs.filter((run) => run.id !== id);
|
|
96
121
|
if (runs.length !== activity.runs.length) {
|
|
97
|
-
writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs }, path);
|
|
122
|
+
writeLocalghostActivity({ ...activity, version: LOCALGHOST_ACTIVITY_VERSION, runs }, path);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function unregisterLocalghostSetup(options, path = getLocalghostActivityPath()) {
|
|
126
|
+
const activity = readLocalghostActivity(path);
|
|
127
|
+
const setups = activity.setups.filter((setup) => {
|
|
128
|
+
if (setup.cwd !== options.cwd) return true;
|
|
129
|
+
if (options.projectName && setup.projectName !== options.projectName) return true;
|
|
130
|
+
if (options.configPath && setup.configPath !== options.configPath) return true;
|
|
131
|
+
return false;
|
|
132
|
+
});
|
|
133
|
+
if (setups.length !== activity.setups.length) {
|
|
134
|
+
writeLocalghostActivity({ ...activity, version: LOCALGHOST_ACTIVITY_VERSION, setups }, path);
|
|
98
135
|
}
|
|
99
136
|
}
|
|
100
137
|
|
|
@@ -288,7 +325,8 @@ async function trustCaddy(path) {
|
|
|
288
325
|
}
|
|
289
326
|
|
|
290
327
|
// src/context.ts
|
|
291
|
-
import { existsSync as existsSync3 } from "fs";
|
|
328
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
|
|
329
|
+
import { join as join4 } from "path";
|
|
292
330
|
import { pathToFileURL } from "url";
|
|
293
331
|
|
|
294
332
|
// src/port.ts
|
|
@@ -317,6 +355,257 @@ async function findAvailablePort(startPort, options = {}) {
|
|
|
317
355
|
throw new Error(`No available port found from ${startPort} to ${startPort + maxAttempts - 1}.`);
|
|
318
356
|
}
|
|
319
357
|
|
|
358
|
+
// src/tunnel.ts
|
|
359
|
+
import { domainToASCII } from "url";
|
|
360
|
+
var DEFAULT_GHOST_TUNNEL_SUBDOMAIN = "ghost";
|
|
361
|
+
var DEFAULT_GHOST_TUNNEL_NAMESPACE_TAGS = ["route", "project", "owner"];
|
|
362
|
+
var DEFAULT_GHOST_TUNNEL_NAMESPACE_SEPARATOR = "-";
|
|
363
|
+
var DEFAULT_GHOST_TUNNEL_MODE = "manual";
|
|
364
|
+
function isResolvedGhostTunnelConfig(value) {
|
|
365
|
+
return typeof value === "object" && value !== null && "enabled" in value;
|
|
366
|
+
}
|
|
367
|
+
function toGhostTunnelConfig(options) {
|
|
368
|
+
return isResolvedGhostTunnelConfig(options) ? options : resolveGhostTunnelConfig(options);
|
|
369
|
+
}
|
|
370
|
+
function stripHostPort(value) {
|
|
371
|
+
const trimmed = value.trim().toLowerCase();
|
|
372
|
+
if (trimmed.startsWith("[") || trimmed.includes("/")) return "";
|
|
373
|
+
const portSeparator = trimmed.lastIndexOf(":");
|
|
374
|
+
if (portSeparator === -1) return trimmed;
|
|
375
|
+
const port = trimmed.slice(portSeparator + 1);
|
|
376
|
+
return /^\d+$/.test(port) ? trimmed.slice(0, portSeparator) : trimmed;
|
|
377
|
+
}
|
|
378
|
+
function normalizeDomain(value) {
|
|
379
|
+
const host = stripHostPort(value.replace(/^\*\./, ""));
|
|
380
|
+
const ascii = domainToASCII(host);
|
|
381
|
+
if (!ascii || ascii.length > 253 || ascii.includes("..")) return null;
|
|
382
|
+
if (ascii.startsWith(".") || ascii.endsWith(".")) return null;
|
|
383
|
+
if (ascii.includes("*")) return null;
|
|
384
|
+
if (!ascii.split(".").every(isValidHostLabel)) return null;
|
|
385
|
+
return ascii;
|
|
386
|
+
}
|
|
387
|
+
function isValidHostLabel(value) {
|
|
388
|
+
return value.length > 0 && value.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(value);
|
|
389
|
+
}
|
|
390
|
+
function isValidNamespaceTag(value) {
|
|
391
|
+
return /^[a-z][a-z0-9_]*$/i.test(value);
|
|
392
|
+
}
|
|
393
|
+
function isNamespaceTagList(options) {
|
|
394
|
+
return Array.isArray(options);
|
|
395
|
+
}
|
|
396
|
+
function assertValidSubdomain(value) {
|
|
397
|
+
if (!isValidHostLabel(value)) {
|
|
398
|
+
throw new Error(`Invalid ghost tunnel subdomain: ${value}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function normalizeDomains(domains) {
|
|
402
|
+
const values = typeof domains === "string" ? [domains] : [...domains ?? []];
|
|
403
|
+
const normalized = values.map((value) => value.trim()).filter(Boolean).map((value) => {
|
|
404
|
+
const domain = normalizeDomain(value);
|
|
405
|
+
if (!domain) throw new Error(`Invalid ghost tunnel domain: ${value}`);
|
|
406
|
+
return domain;
|
|
407
|
+
});
|
|
408
|
+
return [...new Set(normalized)];
|
|
409
|
+
}
|
|
410
|
+
function parseGhostTunnelMode(value) {
|
|
411
|
+
return value ?? DEFAULT_GHOST_TUNNEL_MODE;
|
|
412
|
+
}
|
|
413
|
+
function resolveNamespaceConfig(options) {
|
|
414
|
+
const tags = isNamespaceTagList(options) ? [...options] : [...options?.tags ?? DEFAULT_GHOST_TUNNEL_NAMESPACE_TAGS];
|
|
415
|
+
let separator = DEFAULT_GHOST_TUNNEL_NAMESPACE_SEPARATOR;
|
|
416
|
+
let spreadTag = tags.includes("project") ? "project" : void 0;
|
|
417
|
+
if (options && !isNamespaceTagList(options)) {
|
|
418
|
+
separator = options.separator ?? DEFAULT_GHOST_TUNNEL_NAMESPACE_SEPARATOR;
|
|
419
|
+
spreadTag = options.spreadTag === false ? false : options.spreadTag ?? spreadTag;
|
|
420
|
+
}
|
|
421
|
+
if (tags.length === 0) {
|
|
422
|
+
throw new Error("Ghost tunnel namespace must include at least one tag.");
|
|
423
|
+
}
|
|
424
|
+
for (const tag of tags) {
|
|
425
|
+
if (!isValidNamespaceTag(tag)) {
|
|
426
|
+
throw new Error(`Invalid ghost tunnel namespace tag: ${tag}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (spreadTag && !tags.includes(spreadTag)) {
|
|
430
|
+
throw new Error(`Ghost tunnel namespace spreadTag must be listed in tags: ${spreadTag}`);
|
|
431
|
+
}
|
|
432
|
+
if (!separator || separator.length > 8 || !/^[a-z0-9-]+$/.test(separator)) {
|
|
433
|
+
throw new Error(`Invalid ghost tunnel namespace separator: ${separator}`);
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
tags,
|
|
437
|
+
separator,
|
|
438
|
+
...spreadTag ? { spreadTag } : {}
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function normalizeNamespaceValue(tag, value, separator, options = {}) {
|
|
442
|
+
const normalized = normalizeDomain(value);
|
|
443
|
+
if (!normalized || normalized.includes(".")) {
|
|
444
|
+
throw new Error(`Invalid ghost tunnel namespace value for ${tag}: ${value}`);
|
|
445
|
+
}
|
|
446
|
+
if (!options.allowSeparator && normalized.includes(separator)) {
|
|
447
|
+
throw new Error(`Ghost tunnel namespace value for ${tag} cannot include separator "${separator}": ${value}`);
|
|
448
|
+
}
|
|
449
|
+
return normalized;
|
|
450
|
+
}
|
|
451
|
+
function createNamespaceSlug(config, values) {
|
|
452
|
+
const parts = config.tags.map((tag) => {
|
|
453
|
+
const value = values[tag];
|
|
454
|
+
if (!value) {
|
|
455
|
+
throw new Error(`Missing ghost tunnel namespace value: ${tag}`);
|
|
456
|
+
}
|
|
457
|
+
return normalizeNamespaceValue(tag, value, config.separator, { allowSeparator: tag === config.spreadTag });
|
|
458
|
+
});
|
|
459
|
+
const slug = parts.join(config.separator);
|
|
460
|
+
if (!isValidHostLabel(slug)) {
|
|
461
|
+
throw new Error(`Ghost tunnel namespace is too long for a DNS label: ${slug}`);
|
|
462
|
+
}
|
|
463
|
+
return slug;
|
|
464
|
+
}
|
|
465
|
+
function createNamespaceDisplaySlug(config, values = {}) {
|
|
466
|
+
return config.tags.map((tag) => {
|
|
467
|
+
const value = values[tag];
|
|
468
|
+
if (!value) return `<${tag}>`;
|
|
469
|
+
try {
|
|
470
|
+
return normalizeNamespaceValue(tag, value, config.separator, { allowSeparator: tag === config.spreadTag });
|
|
471
|
+
} catch {
|
|
472
|
+
return `<${tag}>`;
|
|
473
|
+
}
|
|
474
|
+
}).join(config.separator);
|
|
475
|
+
}
|
|
476
|
+
function getPreviewDefaults(preview, defaults) {
|
|
477
|
+
return {
|
|
478
|
+
domain: preview?.domain ?? defaults?.domain,
|
|
479
|
+
route: preview?.route ?? defaults?.route,
|
|
480
|
+
project: preview?.project ?? defaults?.project,
|
|
481
|
+
owner: preview?.owner ?? defaults?.owner,
|
|
482
|
+
values: {
|
|
483
|
+
...defaults?.values ?? {},
|
|
484
|
+
...preview?.values ?? {}
|
|
485
|
+
},
|
|
486
|
+
path: preview?.path,
|
|
487
|
+
protocol: preview?.protocol
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function getDisplayValues(input2) {
|
|
491
|
+
return {
|
|
492
|
+
...input2.route ? { route: input2.route } : {},
|
|
493
|
+
...input2.project ? { project: input2.project } : {},
|
|
494
|
+
...input2.owner ? { owner: input2.owner } : {},
|
|
495
|
+
...input2.values
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function createDisplayUrl(config, defaults, domain) {
|
|
499
|
+
const input2 = getPreviewDefaults(config.preview, defaults);
|
|
500
|
+
const protocol = input2.protocol ?? "https";
|
|
501
|
+
const slug = createNamespaceDisplaySlug(config.namespace, getDisplayValues(input2));
|
|
502
|
+
const entryHost = domain ? getGhostTunnelEntryHost(domain, config) : input2.domain ? getGhostTunnelEntryHost(input2.domain, config) : `${config.subdomain}.*`;
|
|
503
|
+
const url = `${protocol}://${slug}.${entryHost}/`;
|
|
504
|
+
if (!input2.path) return url;
|
|
505
|
+
return `${url}${input2.path.replace(/^\/+/, "")}`;
|
|
506
|
+
}
|
|
507
|
+
function createDisplayUrls(config, defaults) {
|
|
508
|
+
const domains = config.domains.length > 0 ? config.domains : defaults?.domain ? [defaults.domain] : [];
|
|
509
|
+
const urls = domains.length > 0 ? domains.map((domain) => createDisplayUrl(config, defaults, domain)) : [createDisplayUrl(config, defaults)];
|
|
510
|
+
return [...new Set(urls)];
|
|
511
|
+
}
|
|
512
|
+
function maybeConstructPreviewUrl(config, defaults) {
|
|
513
|
+
if (!config.preview) return void 0;
|
|
514
|
+
const input2 = getPreviewDefaults(config.preview, defaults);
|
|
515
|
+
if (!input2.domain || !input2.route || !input2.project || !input2.owner) return void 0;
|
|
516
|
+
return constructGhostTunnelUrl({
|
|
517
|
+
domain: input2.domain,
|
|
518
|
+
route: input2.route,
|
|
519
|
+
project: input2.project,
|
|
520
|
+
owner: input2.owner,
|
|
521
|
+
values: input2.values,
|
|
522
|
+
...input2.path ? { path: input2.path } : {},
|
|
523
|
+
...input2.protocol ? { protocol: input2.protocol } : {},
|
|
524
|
+
ghostTunnel: config
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
function resolveGhostTunnelConfig(options, defaults) {
|
|
528
|
+
if (options === false || typeof options === "undefined") {
|
|
529
|
+
return {
|
|
530
|
+
enabled: false,
|
|
531
|
+
mode: DEFAULT_GHOST_TUNNEL_MODE,
|
|
532
|
+
domains: [],
|
|
533
|
+
subdomain: DEFAULT_GHOST_TUNNEL_SUBDOMAIN,
|
|
534
|
+
namespace: resolveNamespaceConfig(void 0),
|
|
535
|
+
displayUrls: [],
|
|
536
|
+
requireHttps: true,
|
|
537
|
+
requireAuth: true
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
const config = typeof options === "string" ? { mode: options } : options;
|
|
541
|
+
const subdomain = config.subdomain ?? DEFAULT_GHOST_TUNNEL_SUBDOMAIN;
|
|
542
|
+
assertValidSubdomain(subdomain);
|
|
543
|
+
const domains = normalizeDomains(config.domains);
|
|
544
|
+
const enabled = config.enabled ?? true;
|
|
545
|
+
const resolved = {
|
|
546
|
+
enabled,
|
|
547
|
+
mode: parseGhostTunnelMode(config.mode),
|
|
548
|
+
domains,
|
|
549
|
+
subdomain,
|
|
550
|
+
namespace: resolveNamespaceConfig(config.namespace),
|
|
551
|
+
...config.preview ? { preview: config.preview } : {},
|
|
552
|
+
displayUrls: [],
|
|
553
|
+
requireHttps: config.requireHttps ?? true,
|
|
554
|
+
requireAuth: config.requireAuth ?? true
|
|
555
|
+
};
|
|
556
|
+
if (!enabled) {
|
|
557
|
+
return resolved;
|
|
558
|
+
}
|
|
559
|
+
const previewUrl = maybeConstructPreviewUrl(resolved, defaults);
|
|
560
|
+
const displayUrls = previewUrl ? [previewUrl] : createDisplayUrls(resolved, defaults);
|
|
561
|
+
return {
|
|
562
|
+
...resolved,
|
|
563
|
+
displayUrls,
|
|
564
|
+
...displayUrls[0] ? { displayUrl: displayUrls[0] } : {},
|
|
565
|
+
...previewUrl ? { previewUrl } : {}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function getGhostTunnelEntryHost(domain, options = {}) {
|
|
569
|
+
const config = toGhostTunnelConfig(options);
|
|
570
|
+
const normalizedDomain = normalizeDomain(domain);
|
|
571
|
+
if (!normalizedDomain) {
|
|
572
|
+
throw new Error(`Invalid ghost tunnel domain: ${domain}`);
|
|
573
|
+
}
|
|
574
|
+
return `${config.subdomain}.${normalizedDomain}`;
|
|
575
|
+
}
|
|
576
|
+
function constructGhostTunnelHost(input2) {
|
|
577
|
+
const config = toGhostTunnelConfig(input2.ghostTunnel ?? {});
|
|
578
|
+
if (!config.enabled) {
|
|
579
|
+
throw new Error("Ghost tunnel is not enabled.");
|
|
580
|
+
}
|
|
581
|
+
const namespaceValues = {
|
|
582
|
+
route: input2.route,
|
|
583
|
+
project: input2.project,
|
|
584
|
+
owner: input2.owner,
|
|
585
|
+
...input2.values ?? {}
|
|
586
|
+
};
|
|
587
|
+
const slug = createNamespaceSlug(config.namespace, namespaceValues);
|
|
588
|
+
return `${slug}.${getGhostTunnelEntryHost(input2.domain, config)}`;
|
|
589
|
+
}
|
|
590
|
+
function constructGhostTunnelUrl(input2) {
|
|
591
|
+
const protocol = input2.protocol ?? "https";
|
|
592
|
+
const host = constructGhostTunnelHost(input2);
|
|
593
|
+
const url = new URL(`${protocol}://${host}/`);
|
|
594
|
+
if (input2.path) {
|
|
595
|
+
url.pathname = `/${input2.path.replace(/^\/+/, "")}`;
|
|
596
|
+
}
|
|
597
|
+
if (input2.searchParams instanceof URLSearchParams) {
|
|
598
|
+
url.search = input2.searchParams.toString();
|
|
599
|
+
} else if (input2.searchParams) {
|
|
600
|
+
for (const [key, value] of Object.entries(input2.searchParams)) {
|
|
601
|
+
if (typeof value !== "undefined" && value !== null) {
|
|
602
|
+
url.searchParams.set(key, String(value));
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return url.toString();
|
|
607
|
+
}
|
|
608
|
+
|
|
320
609
|
// src/context.ts
|
|
321
610
|
var LOCALGHOST_PROJECT_CONFIG_FILES = [
|
|
322
611
|
"localghost.config.mjs",
|
|
@@ -341,6 +630,25 @@ function envHttps() {
|
|
|
341
630
|
if (!value) return void 0;
|
|
342
631
|
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
343
632
|
}
|
|
633
|
+
function getPackageName(cwd) {
|
|
634
|
+
try {
|
|
635
|
+
const pkg = JSON.parse(readFileSync4(join4(cwd, "package.json"), "utf8"));
|
|
636
|
+
return typeof pkg.name === "string" ? pkg.name : void 0;
|
|
637
|
+
} catch {
|
|
638
|
+
return void 0;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function getPackageOwner(cwd) {
|
|
642
|
+
const packageName = getPackageName(cwd);
|
|
643
|
+
if (!packageName?.startsWith("@")) return void 0;
|
|
644
|
+
return packageName.slice(1).split("/")[0];
|
|
645
|
+
}
|
|
646
|
+
function getLocalOwner(cwd) {
|
|
647
|
+
return sanitizeProjectName(process.env.LOCALGHOST_OWNER ?? getPackageOwner(cwd) ?? process.env.USER ?? process.env.USERNAME ?? "local");
|
|
648
|
+
}
|
|
649
|
+
function getRouteName(primaryHost, fallback) {
|
|
650
|
+
return sanitizeProjectName(primaryHost.split(".")[0] ?? fallback);
|
|
651
|
+
}
|
|
344
652
|
function readOptionsFromContext(options) {
|
|
345
653
|
return {
|
|
346
654
|
cwd: options.cwd ?? process.cwd(),
|
|
@@ -379,18 +687,22 @@ function addDefaultWwwAliases(entries) {
|
|
|
379
687
|
function defined(input2) {
|
|
380
688
|
return Object.fromEntries(Object.entries(input2).filter(([, value]) => typeof value !== "undefined"));
|
|
381
689
|
}
|
|
382
|
-
async function
|
|
383
|
-
|
|
384
|
-
|
|
690
|
+
async function readLocalghostProjectConfig(options = {}) {
|
|
691
|
+
const cwd = options.cwd ?? process.cwd();
|
|
692
|
+
if (options.configFile === false) return { config: {} };
|
|
693
|
+
const candidates = options.configFile ? [options.configFile] : LOCALGHOST_PROJECT_CONFIG_FILES;
|
|
385
694
|
const path = candidates.map((candidate) => resolveDevHostsPath({ cwd, fileName: candidate }).path).find((candidate) => existsSync3(candidate));
|
|
386
|
-
if (!path) return {};
|
|
695
|
+
if (!path) return { config: {} };
|
|
387
696
|
const imported = await import(`${pathToFileURL(path).href}?localghost=${Date.now()}`);
|
|
388
697
|
const config = imported.default ?? imported;
|
|
389
698
|
return { config, path };
|
|
390
699
|
}
|
|
391
700
|
async function resolveLocalghostContext(options = {}) {
|
|
392
701
|
const cwd = options.cwd ?? process.cwd();
|
|
393
|
-
const projectConfig = await
|
|
702
|
+
const projectConfig = await readLocalghostProjectConfig({
|
|
703
|
+
cwd,
|
|
704
|
+
...typeof options.localghostConfig !== "undefined" ? { configFile: options.localghostConfig } : {}
|
|
705
|
+
});
|
|
394
706
|
const merged = {
|
|
395
707
|
...projectConfig.config,
|
|
396
708
|
...defined(options)
|
|
@@ -399,7 +711,7 @@ async function resolveLocalghostContext(options = {}) {
|
|
|
399
711
|
const resolvedPath = resolveDevHostsPath(readOptions);
|
|
400
712
|
const configEntries = readDevHosts(readOptions);
|
|
401
713
|
const requestedPort = merged.port ?? envPort() ?? configEntries[0]?.port ?? 5173;
|
|
402
|
-
const dynamicPort = merged.dynamicPort ?? envDynamicPort() ??
|
|
714
|
+
const dynamicPort = merged.dynamicPort ?? envDynamicPort() ?? true;
|
|
403
715
|
const bindHost = merged.bindHost ?? "127.0.0.1";
|
|
404
716
|
const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
|
|
405
717
|
const port = dynamicPort ? await findAvailablePort(requestedPort, { host: probeHost }) : requestedPort;
|
|
@@ -407,9 +719,15 @@ async function resolveLocalghostContext(options = {}) {
|
|
|
407
719
|
const entries = wwwAlias ? addDefaultWwwAliases(withRuntimePort(configEntries, requestedPort, port)) : withRuntimePort(configEntries, requestedPort, port);
|
|
408
720
|
const hosts = uniqueHosts(entries);
|
|
409
721
|
const primaryHost = merged.primaryHost ?? entries.find((entry) => entry.port === port)?.host ?? hosts[0] ?? `${sanitizeProjectName(getProjectName(cwd))}.localhost`;
|
|
722
|
+
const projectName = sanitizeProjectName(merged.project ?? getProjectName(cwd));
|
|
723
|
+
const ghostTunnel = resolveGhostTunnelConfig(merged.ghostTunnel, {
|
|
724
|
+
route: getRouteName(primaryHost, projectName),
|
|
725
|
+
project: projectName,
|
|
726
|
+
owner: getLocalOwner(cwd)
|
|
727
|
+
});
|
|
410
728
|
return {
|
|
411
729
|
cwd,
|
|
412
|
-
projectName
|
|
730
|
+
projectName,
|
|
413
731
|
readOptions,
|
|
414
732
|
configPath: resolvedPath.path,
|
|
415
733
|
configFileName: resolvedPath.fileName,
|
|
@@ -423,6 +741,7 @@ async function resolveLocalghostContext(options = {}) {
|
|
|
423
741
|
primaryHost,
|
|
424
742
|
https: merged.https ?? envHttps() ?? false,
|
|
425
743
|
wwwAlias,
|
|
744
|
+
ghostTunnel,
|
|
426
745
|
...projectConfig.path ? { projectConfigPath: projectConfig.path } : {}
|
|
427
746
|
};
|
|
428
747
|
}
|
|
@@ -473,7 +792,7 @@ function assertLocalDevelopment(command, env = process.env) {
|
|
|
473
792
|
// src/hosts-file.ts
|
|
474
793
|
import { writeFileSync as writeFileSync3 } from "fs";
|
|
475
794
|
import { tmpdir } from "os";
|
|
476
|
-
import { join as
|
|
795
|
+
import { join as join5 } from "path";
|
|
477
796
|
import { execa as execa3 } from "execa";
|
|
478
797
|
function escapeRegExp(value) {
|
|
479
798
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -515,7 +834,7 @@ function removeManagedBlock(existing, projectName) {
|
|
|
515
834
|
}
|
|
516
835
|
async function writeSystemHostsFile(hostsPath, next, projectName) {
|
|
517
836
|
const sanitizedProjectName = sanitizeProjectName(projectName);
|
|
518
|
-
const tempPath =
|
|
837
|
+
const tempPath = join5(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
|
|
519
838
|
writeFileSync3(tempPath, next, "utf8");
|
|
520
839
|
if (process.platform === "win32") {
|
|
521
840
|
throw new Error(`Windows support: run as administrator and copy ${tempPath} to ${hostsPath}.`);
|
|
@@ -548,11 +867,11 @@ async function removeSystemHosts(projectName) {
|
|
|
548
867
|
}
|
|
549
868
|
|
|
550
869
|
// src/init.ts
|
|
551
|
-
import { existsSync as existsSync4, readFileSync as
|
|
552
|
-
import { join as
|
|
870
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
871
|
+
import { join as join6 } from "path";
|
|
553
872
|
function detectPackageManager(cwd = process.cwd()) {
|
|
554
|
-
if (existsSync4(
|
|
555
|
-
if (existsSync4(
|
|
873
|
+
if (existsSync4(join6(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
874
|
+
if (existsSync4(join6(cwd, "yarn.lock"))) return "yarn";
|
|
556
875
|
return "npm";
|
|
557
876
|
}
|
|
558
877
|
function packageRunCommand(packageManager, script) {
|
|
@@ -572,7 +891,7 @@ function renderConfig(options) {
|
|
|
572
891
|
}
|
|
573
892
|
function readPackageJson(path) {
|
|
574
893
|
try {
|
|
575
|
-
return JSON.parse(
|
|
894
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
576
895
|
} catch {
|
|
577
896
|
return null;
|
|
578
897
|
}
|
|
@@ -624,7 +943,7 @@ function initLocalghost(options = {}) {
|
|
|
624
943
|
const apiPort = options.apiPort ?? 8787;
|
|
625
944
|
const packageManager = options.packageManager ?? detectPackageManager(cwd);
|
|
626
945
|
const configFile = options.configFile ?? LOCALGHOST_CONFIG_FILE;
|
|
627
|
-
const configPath =
|
|
946
|
+
const configPath = join6(cwd, configFile);
|
|
628
947
|
const configExists = existsSync4(configPath);
|
|
629
948
|
if (configExists && !options.force) {
|
|
630
949
|
return {
|
|
@@ -641,7 +960,7 @@ function initLocalghost(options = {}) {
|
|
|
641
960
|
};
|
|
642
961
|
}
|
|
643
962
|
writeTextFile(configPath, renderConfig({ host, port, apiHost, apiPort }));
|
|
644
|
-
const packageJsonPath =
|
|
963
|
+
const packageJsonPath = join6(cwd, "package.json");
|
|
645
964
|
const packageJsonChanged = options.writeScripts ? updatePackageScripts(packageJsonPath, configFile) : false;
|
|
646
965
|
return {
|
|
647
966
|
configPath,
|
|
@@ -682,6 +1001,20 @@ async function confirm(question, defaultValue = true) {
|
|
|
682
1001
|
}
|
|
683
1002
|
|
|
684
1003
|
// src/routes.ts
|
|
1004
|
+
var ansi = {
|
|
1005
|
+
cyan: "\x1B[36m",
|
|
1006
|
+
dim: "\x1B[2m",
|
|
1007
|
+
green: "\x1B[32m",
|
|
1008
|
+
reset: "\x1B[0m",
|
|
1009
|
+
yellow: "\x1B[33m"
|
|
1010
|
+
};
|
|
1011
|
+
function colorize(value, color, enabled) {
|
|
1012
|
+
return enabled ? `${color}${value}${ansi.reset}` : value;
|
|
1013
|
+
}
|
|
1014
|
+
function colorizeUrl(value, enabled) {
|
|
1015
|
+
if (!enabled) return value;
|
|
1016
|
+
return colorize(value.replace(/\*/g, `${ansi.yellow}*${ansi.cyan}`), ansi.cyan, enabled);
|
|
1017
|
+
}
|
|
685
1018
|
function getDomainRoutes(entries, options = {}) {
|
|
686
1019
|
const protocol = options.https === true ? "https" : "http";
|
|
687
1020
|
return [...entries].sort((left, right) => left.host.localeCompare(right.host) || left.port - right.port).map((entry) => ({
|
|
@@ -701,13 +1034,40 @@ function formatDomainRoutes(entries, options = {}) {
|
|
|
701
1034
|
...routes.map((route) => ` ${route.url} -> ${route.upstream}`)
|
|
702
1035
|
].join("\n");
|
|
703
1036
|
}
|
|
1037
|
+
function formatGhostTunnel(config, options = {}) {
|
|
1038
|
+
if (!config.enabled) return null;
|
|
1039
|
+
const color = options.color === true;
|
|
1040
|
+
const label = options.label ?? "expected";
|
|
1041
|
+
const labelColor = label === "running" ? ansi.green : ansi.dim;
|
|
1042
|
+
const lines = [
|
|
1043
|
+
"localghost ghost tunnel",
|
|
1044
|
+
` mode: ${config.mode}`
|
|
1045
|
+
];
|
|
1046
|
+
const urls = config.displayUrls.length > 0 ? config.displayUrls : config.displayUrl ? [config.displayUrl] : [];
|
|
1047
|
+
if (urls.length === 0) {
|
|
1048
|
+
lines.push(` ${label}: unavailable`);
|
|
1049
|
+
} else if (urls.length === 1) {
|
|
1050
|
+
lines.push(` ${colorize(label, labelColor, color)}: ${colorizeUrl(urls[0], color)}`);
|
|
1051
|
+
} else {
|
|
1052
|
+
lines.push(` ${colorize(label, labelColor, color)}:`);
|
|
1053
|
+
for (const url of urls) {
|
|
1054
|
+
lines.push(` ${colorizeUrl(url, color)}`);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (options.verbose) {
|
|
1058
|
+
lines.push(` domains: ${config.domains.length > 0 ? config.domains.join(", ") : "*"}`);
|
|
1059
|
+
lines.push(` access: ${config.requireAuth ? "auth required" : "app decides"}`);
|
|
1060
|
+
lines.push(` transport: ${config.requireHttps ? "https required" : "http allowed"}`);
|
|
1061
|
+
}
|
|
1062
|
+
return lines.join("\n");
|
|
1063
|
+
}
|
|
704
1064
|
|
|
705
1065
|
// src/state.ts
|
|
706
1066
|
import { existsSync as existsSync5 } from "fs";
|
|
707
|
-
import { join as
|
|
1067
|
+
import { join as join7 } from "path";
|
|
708
1068
|
var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
|
|
709
1069
|
function getLocalghostStatePath(cwd = process.cwd()) {
|
|
710
|
-
return
|
|
1070
|
+
return join7(cwd, LOCALGHOST_STATE_FILE);
|
|
711
1071
|
}
|
|
712
1072
|
function readLocalghostState(cwd = process.cwd()) {
|
|
713
1073
|
const path = getLocalghostStatePath(cwd);
|
|
@@ -727,11 +1087,11 @@ function patchLocalghostState(cwd, patch) {
|
|
|
727
1087
|
}
|
|
728
1088
|
|
|
729
1089
|
// src/update-check.ts
|
|
730
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as
|
|
1090
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
731
1091
|
import { homedir as homedir2 } from "os";
|
|
732
|
-
import { dirname as dirname4, join as
|
|
1092
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
733
1093
|
var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
|
|
734
|
-
var LOCALGHOST_VERSION = "0.1.
|
|
1094
|
+
var LOCALGHOST_VERSION = "0.1.9";
|
|
735
1095
|
var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
736
1096
|
var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
737
1097
|
var UPDATE_CHECK_TIMEOUT_MS = 900;
|
|
@@ -743,13 +1103,13 @@ function isUpdateCheckDisabled(env = process.env) {
|
|
|
743
1103
|
}
|
|
744
1104
|
function getUpdateCheckCachePath(env = process.env) {
|
|
745
1105
|
if (env.LOCALGHOST_UPDATE_CHECK_CACHE) return env.LOCALGHOST_UPDATE_CHECK_CACHE;
|
|
746
|
-
const cacheRoot = env.XDG_CACHE_HOME ||
|
|
747
|
-
return
|
|
1106
|
+
const cacheRoot = env.XDG_CACHE_HOME || join8(homedir2(), ".cache");
|
|
1107
|
+
return join8(cacheRoot, "localghost", "update-check.json");
|
|
748
1108
|
}
|
|
749
1109
|
function readCache(path = getUpdateCheckCachePath()) {
|
|
750
1110
|
if (!existsSync6(path)) return null;
|
|
751
1111
|
try {
|
|
752
|
-
return JSON.parse(
|
|
1112
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
753
1113
|
} catch {
|
|
754
1114
|
return null;
|
|
755
1115
|
}
|
|
@@ -903,8 +1263,18 @@ function warnAboutLocalMdns(entries) {
|
|
|
903
1263
|
);
|
|
904
1264
|
}
|
|
905
1265
|
}
|
|
1266
|
+
function shouldColor() {
|
|
1267
|
+
return process.stdout.isTTY && !process.env.NO_COLOR;
|
|
1268
|
+
}
|
|
906
1269
|
function logDomainRoutes(entries, options = {}) {
|
|
907
1270
|
console.log(formatDomainRoutes(entries, options));
|
|
1271
|
+
if (options.ghostTunnel?.enabled) {
|
|
1272
|
+
console.log(formatGhostTunnel(options.ghostTunnel, {
|
|
1273
|
+
color: shouldColor(),
|
|
1274
|
+
label: "expected",
|
|
1275
|
+
verbose: options.verbose === true
|
|
1276
|
+
}));
|
|
1277
|
+
}
|
|
908
1278
|
}
|
|
909
1279
|
function parsePort2(value) {
|
|
910
1280
|
const port = Number.parseInt(value, 10);
|
|
@@ -998,7 +1368,7 @@ function getSetupReadiness(options) {
|
|
|
998
1368
|
}
|
|
999
1369
|
const hostsPath = getSystemHostsPath();
|
|
1000
1370
|
try {
|
|
1001
|
-
const hosts =
|
|
1371
|
+
const hosts = readFileSync7(hostsPath, "utf8");
|
|
1002
1372
|
const expectedHostsBlock = renderHostsBlock(projectName, entries).trimEnd();
|
|
1003
1373
|
if (!hosts.includes(expectedHostsBlock)) {
|
|
1004
1374
|
reasons.push(`The Localghost hosts block in ${hostsPath} is missing or stale.`);
|
|
@@ -1012,7 +1382,7 @@ function getSetupReadiness(options) {
|
|
|
1012
1382
|
reasons.push(`Missing Caddyfile at ${caddyfilePath}.`);
|
|
1013
1383
|
} else {
|
|
1014
1384
|
const expectedCaddyfile = renderCaddyfile(entries, { https });
|
|
1015
|
-
const currentCaddyfile =
|
|
1385
|
+
const currentCaddyfile = readFileSync7(caddyfilePath, "utf8");
|
|
1016
1386
|
if (currentCaddyfile !== expectedCaddyfile) {
|
|
1017
1387
|
reasons.push(`Caddyfile at ${caddyfilePath} is stale for ${https ? "HTTPS" : "HTTP"} mode.`);
|
|
1018
1388
|
}
|
|
@@ -1047,6 +1417,14 @@ async function runSetupFromReadiness(cwd, https, readiness) {
|
|
|
1047
1417
|
...existingTrustMarkers(cwd),
|
|
1048
1418
|
entries: readiness.entries
|
|
1049
1419
|
});
|
|
1420
|
+
registerLocalghostSetup({
|
|
1421
|
+
cwd,
|
|
1422
|
+
projectName: readiness.projectName,
|
|
1423
|
+
configPath: readiness.configPath,
|
|
1424
|
+
caddyfilePath,
|
|
1425
|
+
https,
|
|
1426
|
+
entries: readiness.entries
|
|
1427
|
+
});
|
|
1050
1428
|
}
|
|
1051
1429
|
function wait(ms) {
|
|
1052
1430
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
@@ -1096,35 +1474,91 @@ function registerCleanup(id) {
|
|
|
1096
1474
|
process.off("exit", cleanup);
|
|
1097
1475
|
};
|
|
1098
1476
|
}
|
|
1099
|
-
async function
|
|
1477
|
+
async function getRouteViews(entries) {
|
|
1100
1478
|
const portStatus = /* @__PURE__ */ new Map();
|
|
1101
|
-
for (const entry of
|
|
1479
|
+
for (const entry of entries) {
|
|
1102
1480
|
if (!portStatus.has(entry.port)) {
|
|
1103
1481
|
portStatus.set(entry.port, !await isPortAvailable(entry.port));
|
|
1104
1482
|
}
|
|
1105
1483
|
}
|
|
1484
|
+
return entries.map((entry) => ({
|
|
1485
|
+
host: entry.host,
|
|
1486
|
+
port: entry.port,
|
|
1487
|
+
target: `127.0.0.1:${entry.port}`,
|
|
1488
|
+
listening: portStatus.get(entry.port) ?? false
|
|
1489
|
+
}));
|
|
1490
|
+
}
|
|
1491
|
+
function setupKey(input2) {
|
|
1492
|
+
return `${input2.projectName}:${input2.cwd}:${input2.configPath ?? ""}`;
|
|
1493
|
+
}
|
|
1494
|
+
function runKey(input2) {
|
|
1495
|
+
return `${input2.projectName}:${input2.cwd}:${input2.configPath ?? ""}`;
|
|
1496
|
+
}
|
|
1497
|
+
async function getInstanceViews(setups, runs) {
|
|
1498
|
+
const runBySetup = new Map(runs.map((run) => [runKey(run), run]));
|
|
1499
|
+
const instances = [];
|
|
1500
|
+
for (const setup of setups) {
|
|
1501
|
+
const run = runBySetup.get(setupKey(setup));
|
|
1502
|
+
if (run) {
|
|
1503
|
+
instances.push(await getRunInstanceView(run, setup));
|
|
1504
|
+
runBySetup.delete(setupKey(setup));
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
instances.push({
|
|
1508
|
+
id: setup.id,
|
|
1509
|
+
cwd: setup.cwd,
|
|
1510
|
+
projectName: setup.projectName,
|
|
1511
|
+
running: false,
|
|
1512
|
+
mode: "setup",
|
|
1513
|
+
updatedAt: setup.updatedAt,
|
|
1514
|
+
...setup.configPath ? { configPath: setup.configPath } : {},
|
|
1515
|
+
...setup.caddyfilePath ? { caddyfilePath: setup.caddyfilePath } : {},
|
|
1516
|
+
...typeof setup.https === "boolean" ? { https: setup.https } : {},
|
|
1517
|
+
routes: await getRouteViews(setup.entries)
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
for (const run of runBySetup.values()) {
|
|
1521
|
+
instances.push(await getRunInstanceView(run));
|
|
1522
|
+
}
|
|
1523
|
+
return instances.sort((left, right) => {
|
|
1524
|
+
if (left.running !== right.running) return left.running ? -1 : 1;
|
|
1525
|
+
return left.projectName.localeCompare(right.projectName);
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
async function getRunInstanceView(run, setup) {
|
|
1106
1529
|
return {
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1530
|
+
id: setup?.id ?? run.id,
|
|
1531
|
+
cwd: run.cwd,
|
|
1532
|
+
projectName: run.projectName,
|
|
1533
|
+
running: true,
|
|
1534
|
+
mode: run.mode,
|
|
1535
|
+
updatedAt: setup?.updatedAt ?? run.updatedAt,
|
|
1536
|
+
startedAt: run.startedAt,
|
|
1537
|
+
pid: run.pid,
|
|
1538
|
+
...run.caddyPid ? { caddyPid: run.caddyPid } : {},
|
|
1539
|
+
...run.childPid ? { childPid: run.childPid } : {},
|
|
1540
|
+
...run.childCommand ? { childCommand: run.childCommand } : {},
|
|
1541
|
+
...run.configPath ? { configPath: run.configPath } : {},
|
|
1542
|
+
...run.caddyfilePath ? { caddyfilePath: run.caddyfilePath } : {},
|
|
1543
|
+
...typeof run.https === "boolean" ? { https: run.https } : {},
|
|
1544
|
+
routes: await getRouteViews(run.entries)
|
|
1114
1545
|
};
|
|
1115
1546
|
}
|
|
1116
|
-
function
|
|
1117
|
-
if (
|
|
1547
|
+
function formatInstanceViews(instances) {
|
|
1548
|
+
if (instances.length === 0) return "No Localghost setups found.";
|
|
1118
1549
|
const lines = ["localghost ps"];
|
|
1119
|
-
for (const
|
|
1120
|
-
const command =
|
|
1121
|
-
const mode = command ? `${
|
|
1550
|
+
for (const instance of instances) {
|
|
1551
|
+
const command = instance.childCommand?.length ? ` ${instance.childCommand.join(" ")}` : "";
|
|
1552
|
+
const mode = command ? `${instance.mode}:${command}` : instance.mode === "setup" ? "" : instance.mode;
|
|
1122
1553
|
lines.push("");
|
|
1123
|
-
lines.push(`${
|
|
1124
|
-
lines.push(` cwd: ${
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1554
|
+
lines.push(`${instance.projectName} ${instance.running ? "running" : "setup"}${mode ? ` ${mode}` : ""}`);
|
|
1555
|
+
lines.push(` cwd: ${instance.cwd}`);
|
|
1556
|
+
if (instance.pid) {
|
|
1557
|
+
lines.push(` pid: ${instance.pid}${instance.caddyPid ? `, caddy: ${instance.caddyPid}` : ""}${instance.childPid ? `, child: ${instance.childPid}` : ""}`);
|
|
1558
|
+
}
|
|
1559
|
+
if (instance.startedAt) lines.push(` started: ${instance.startedAt}`);
|
|
1560
|
+
if (!instance.startedAt && instance.updatedAt) lines.push(` setup: ${instance.updatedAt}`);
|
|
1561
|
+
for (const route of instance.routes) {
|
|
1128
1562
|
lines.push(` ${route.host} -> ${route.target} (${route.listening ? "listening" : "not listening"})`);
|
|
1129
1563
|
}
|
|
1130
1564
|
}
|
|
@@ -1198,7 +1632,7 @@ program.command("setup").description("Update /etc/hosts and generate/validate Ca
|
|
|
1198
1632
|
const configPath = context.configPath;
|
|
1199
1633
|
const entries = context.entries;
|
|
1200
1634
|
warnAboutLocalMdns(entries);
|
|
1201
|
-
logDomainRoutes(entries, { https });
|
|
1635
|
+
logDomainRoutes(entries, { https, ghostTunnel: context.ghostTunnel });
|
|
1202
1636
|
explainHostsPassword();
|
|
1203
1637
|
const hostsResult = await updateSystemHosts(projectName, entries);
|
|
1204
1638
|
if (hostsResult.changed) {
|
|
@@ -1221,6 +1655,14 @@ program.command("setup").description("Update /etc/hosts and generate/validate Ca
|
|
|
1221
1655
|
...existingTrustMarkers(options.cwd),
|
|
1222
1656
|
entries
|
|
1223
1657
|
});
|
|
1658
|
+
registerLocalghostSetup({
|
|
1659
|
+
cwd: options.cwd,
|
|
1660
|
+
projectName,
|
|
1661
|
+
configPath,
|
|
1662
|
+
caddyfilePath: caddyfile,
|
|
1663
|
+
https,
|
|
1664
|
+
entries
|
|
1665
|
+
});
|
|
1224
1666
|
console.log(`Generated ${caddyfile}`);
|
|
1225
1667
|
console.log(`Mode ${https ? "HTTPS" : "HTTP"}`);
|
|
1226
1668
|
console.log(`State ${statePath}`);
|
|
@@ -1234,7 +1676,7 @@ program.command("trust").description("Trust Caddy's local HTTPS CA for this proj
|
|
|
1234
1676
|
throw new Error("Localghost HTTPS is not enabled for this context. Set https: true in localghost.config.mjs or pass --https.");
|
|
1235
1677
|
}
|
|
1236
1678
|
warnAboutLocalMdns(context.entries);
|
|
1237
|
-
logDomainRoutes(context.entries, { https: true });
|
|
1679
|
+
logDomainRoutes(context.entries, { https: true, ghostTunnel: context.ghostTunnel });
|
|
1238
1680
|
explainTrustPassword();
|
|
1239
1681
|
const caddyfile = await writeCaddyfile(context.entries, options.cwd, { https: true });
|
|
1240
1682
|
await validateCaddyfile(caddyfile);
|
|
@@ -1264,6 +1706,7 @@ program.command("reset").description("Remove Localghost setup state without dele
|
|
|
1264
1706
|
} else {
|
|
1265
1707
|
console.log(`No Localghost hosts block found in ${hostsResult.hostsPath}`);
|
|
1266
1708
|
}
|
|
1709
|
+
unregisterLocalghostSetup({ cwd: options.cwd, projectName });
|
|
1267
1710
|
console.log(".localghost was left in place. Run localghost setup when you are ready.");
|
|
1268
1711
|
});
|
|
1269
1712
|
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) => {
|
|
@@ -1295,6 +1738,7 @@ program.command("teardown").description("Remove Localghost's managed /etc/hosts
|
|
|
1295
1738
|
if (options.removeCaddyfile) {
|
|
1296
1739
|
console.log(caddyfileRemoved ? `Removed ${caddyfilePath}` : `${caddyfilePath} was not present`);
|
|
1297
1740
|
}
|
|
1741
|
+
unregisterLocalghostSetup({ cwd: options.cwd, projectName });
|
|
1298
1742
|
console.log(`State ${statePath}`);
|
|
1299
1743
|
});
|
|
1300
1744
|
program.command("status").description("Print Localghost's project-local state file").option("--project <name>", "Managed /etc/hosts block name").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to look for. Can be repeated.", collect, []).option("--config-pattern <regex>", "Regex for config filenames in the project root").option("--ready", "Exit non-zero when setup is missing or stale").option("--https", "Check setup readiness for HTTPS mode").option("--ssl", "Alias for --https").option("--json", "Print raw JSON").action(async (options) => {
|
|
@@ -1340,10 +1784,17 @@ program.command("status").description("Print Localghost's project-local state fi
|
|
|
1340
1784
|
process.exitCode = 1;
|
|
1341
1785
|
}
|
|
1342
1786
|
});
|
|
1343
|
-
program.command("routes").description("Print domain to upstream routes").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to look for. Can be repeated.", collect, []).option("--config-pattern <regex>", "Regex for config filenames in the project root").option("--http", "Print domain URLs with http instead of https").option("--https", "Print domain URLs with https").option("--ssl", "Alias for --https").action(async (options) => {
|
|
1787
|
+
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").option("--verbose", "Print Ghost Tunnel mode, domains, and guardrails").action(async (options) => {
|
|
1344
1788
|
const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
|
|
1345
1789
|
warnAboutLocalMdns(context.entries);
|
|
1346
1790
|
console.log(formatDomainRoutes(context.entries, { https: options.http ? false : context.https }));
|
|
1791
|
+
if (context.ghostTunnel.enabled) {
|
|
1792
|
+
console.log(formatGhostTunnel(context.ghostTunnel, {
|
|
1793
|
+
color: shouldColor(),
|
|
1794
|
+
label: "expected",
|
|
1795
|
+
verbose: options.verbose === true
|
|
1796
|
+
}));
|
|
1797
|
+
}
|
|
1347
1798
|
});
|
|
1348
1799
|
program.command("dev").description("Run the Localghost Caddy proxy after setup").option("--project <name>", "Managed /etc/hosts block name").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to look for. Can be repeated.", collect, []).option("--config-pattern <regex>", "Regex for config filenames in the project root").option("--https", "Run a local HTTPS proxy with Caddy local certificates").option("--ssl", "Alias for --https").option("--setup", "Run setup before starting the proxy when setup is missing or stale").option("--trust", "Trust Caddy's local HTTPS CA before starting the proxy").action(async (options) => {
|
|
1349
1800
|
assertLocalDevelopment("dev");
|
|
@@ -1385,9 +1836,17 @@ program.command("dev").description("Run the Localghost Caddy proxy after setup")
|
|
|
1385
1836
|
...existingTrustMarkers(options.cwd),
|
|
1386
1837
|
entries: readiness.entries
|
|
1387
1838
|
});
|
|
1839
|
+
registerLocalghostSetup({
|
|
1840
|
+
cwd: options.cwd,
|
|
1841
|
+
projectName: readiness.projectName,
|
|
1842
|
+
configPath: readiness.configPath,
|
|
1843
|
+
caddyfilePath,
|
|
1844
|
+
https,
|
|
1845
|
+
entries: readiness.entries
|
|
1846
|
+
});
|
|
1388
1847
|
}
|
|
1389
1848
|
warnAboutLocalMdns(readiness.entries);
|
|
1390
|
-
logDomainRoutes(readiness.entries, { https });
|
|
1849
|
+
logDomainRoutes(readiness.entries, { https, ghostTunnel: context.ghostTunnel });
|
|
1391
1850
|
const caddyfile = await writeCaddyfile(readiness.entries, options.cwd, { https });
|
|
1392
1851
|
await validateCaddyfile(caddyfile);
|
|
1393
1852
|
const caddy = startCaddy(caddyfile);
|
|
@@ -1420,7 +1879,7 @@ program.command("dev").description("Run the Localghost Caddy proxy after setup")
|
|
|
1420
1879
|
cleanupRun();
|
|
1421
1880
|
}
|
|
1422
1881
|
});
|
|
1423
|
-
program.command("run").description("Run Caddy and a dev command from the same Localghost context").option("--project <name>", "Managed /etc/hosts block name").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to look for. Can be repeated.", collect, []).option("--config-pattern <regex>", "Regex for config filenames in the project root").option("--port <number>", "Initial app port", parsePort2).option("--https", "Run a local HTTPS proxy with Caddy local certificates").option("--ssl", "Alias for --https").option("--setup", "Run setup before starting when setup is missing or stale").option("--trust", "Trust Caddy's local HTTPS CA before starting the child command").option("--dynamic-port [yes|no]", "Use the requested port if free, otherwise continue upward", parseBooleanLike
|
|
1882
|
+
program.command("run").description("Run Caddy and a dev command from the same Localghost context").option("--project <name>", "Managed /etc/hosts block name").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to look for. Can be repeated.", collect, []).option("--config-pattern <regex>", "Regex for config filenames in the project root").option("--port <number>", "Initial app port", parsePort2).option("--https", "Run a local HTTPS proxy with Caddy local certificates").option("--ssl", "Alias for --https").option("--setup", "Run setup before starting when setup is missing or stale").option("--trust", "Trust Caddy's local HTTPS CA before starting the child command").option("--dynamic-port [yes|no]", "Use the requested port if free, otherwise continue upward", parseBooleanLike).argument("<command...>", "Command to run after --, for example: localghost run -- vite").action(async (command, options) => {
|
|
1424
1883
|
assertLocalDevelopment("run");
|
|
1425
1884
|
await assertCaddyReady();
|
|
1426
1885
|
const context = await resolveLocalghostContext({
|
|
@@ -1459,7 +1918,7 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
|
|
|
1459
1918
|
console.log(`Port ${context.requestedPort} is busy; using ${context.port}.`);
|
|
1460
1919
|
}
|
|
1461
1920
|
warnAboutLocalMdns(context.entries);
|
|
1462
|
-
logDomainRoutes(context.entries, { https });
|
|
1921
|
+
logDomainRoutes(context.entries, { https, ghostTunnel: context.ghostTunnel });
|
|
1463
1922
|
const caddyfile = await writeCaddyfile(context.entries, options.cwd, { https });
|
|
1464
1923
|
await validateCaddyfile(caddyfile);
|
|
1465
1924
|
const caddy = startCaddy(caddyfile);
|
|
@@ -1524,13 +1983,15 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
|
|
|
1524
1983
|
cleanupRun();
|
|
1525
1984
|
}
|
|
1526
1985
|
});
|
|
1527
|
-
program.command("ps").description("Show Localghost
|
|
1528
|
-
const
|
|
1986
|
+
program.command("ps").description("Show Localghost setups and currently running sessions").option("--json", "Print raw JSON").action(async (options) => {
|
|
1987
|
+
const setups = listLocalghostSetups();
|
|
1988
|
+
const runs = listLocalghostRuns();
|
|
1989
|
+
const instances = await getInstanceViews(setups, runs);
|
|
1529
1990
|
if (options.json) {
|
|
1530
|
-
console.log(JSON.stringify({ activityPath: getLocalghostActivityPath(), runs }, null, 2));
|
|
1991
|
+
console.log(JSON.stringify({ activityPath: getLocalghostActivityPath(), setups, runs, instances }, null, 2));
|
|
1531
1992
|
return;
|
|
1532
1993
|
}
|
|
1533
|
-
console.log(
|
|
1994
|
+
console.log(formatInstanceViews(instances));
|
|
1534
1995
|
});
|
|
1535
1996
|
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) => {
|
|
1536
1997
|
const entries = readDevHosts(readOptionsFromCli(options));
|