@hamedb89/localghost 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -327
- package/apps/macos-widget/LocalghostWidget.entitlements +12 -0
- package/apps/macos-widget/LocalghostWidget.swift +701 -48
- package/apps/macos-widget/Resources/localghost-logo-source.png +0 -0
- package/apps/macos-widget/Resources/localghost-widget-ui-reference.png +0 -0
- package/apps/macos-widget/Shared/LocalghostWidgetSnapshot.swift +50 -0
- package/apps/macos-widget/WidgetExtension/LocalghostDesktopWidget.swift +176 -0
- package/apps/macos-widget/WidgetExtension/LocalghostDesktopWidgetExtension.entitlements +12 -0
- package/apps/macos-widget/build.sh +8 -1
- package/apps/macos-widget/project.yml +40 -0
- package/assets/ghost-tunnel-app-icon.png +0 -0
- package/assets/ghost-tunnel-portal.png +0 -0
- package/assets/ghost-tunnel-wordmark.png +0 -0
- package/dist/cli.js +528 -63
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +130 -5
- package/dist/index.js +765 -31
- package/dist/index.js.map +1 -1
- package/dist/tunnel-DzfLXZ8O.d.ts +126 -0
- package/dist/vite.d.ts +3 -1
- package/dist/vite.js +660 -36
- package/dist/vite.js.map +1 -1
- package/docs/flows.md +19 -0
- package/docs/ghost-tunnel.md +293 -0
- package/docs/github.md +6 -6
- package/docs/localghost.1.md +10 -4
- package/docs/macos-widget.md +68 -6
- package/package.json +6 -2
- package/dist/config-Cde1Bich.d.ts +0 -31
package/dist/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,261 @@ 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 getDisplayDefaults(config, defaults) {
|
|
499
|
+
return config.mode === "public" && !config.preview ? void 0 : defaults;
|
|
500
|
+
}
|
|
501
|
+
function createDisplayUrl(config, defaults, domain) {
|
|
502
|
+
const input2 = getPreviewDefaults(config.preview, getDisplayDefaults(config, defaults));
|
|
503
|
+
const protocol = input2.protocol ?? "https";
|
|
504
|
+
const slug = createNamespaceDisplaySlug(config.namespace, getDisplayValues(input2));
|
|
505
|
+
const entryHost = domain ? getGhostTunnelEntryHost(domain, config) : input2.domain ? getGhostTunnelEntryHost(input2.domain, config) : `${config.subdomain}.*`;
|
|
506
|
+
const url = `${protocol}://${slug}.${entryHost}/`;
|
|
507
|
+
if (!input2.path) return url;
|
|
508
|
+
return `${url}${input2.path.replace(/^\/+/, "")}`;
|
|
509
|
+
}
|
|
510
|
+
function createDisplayUrls(config, defaults) {
|
|
511
|
+
const displayDefaults = getDisplayDefaults(config, defaults);
|
|
512
|
+
const domains = config.domains.length > 0 ? config.domains : displayDefaults?.domain ? [displayDefaults.domain] : [];
|
|
513
|
+
const urls = domains.length > 0 ? domains.map((domain) => createDisplayUrl(config, displayDefaults, domain)) : [createDisplayUrl(config, displayDefaults)];
|
|
514
|
+
return [...new Set(urls)];
|
|
515
|
+
}
|
|
516
|
+
function maybeConstructPreviewUrl(config, defaults) {
|
|
517
|
+
if (!config.preview) return void 0;
|
|
518
|
+
const input2 = getPreviewDefaults(config.preview, defaults);
|
|
519
|
+
if (!input2.domain || !input2.route || !input2.project || !input2.owner) return void 0;
|
|
520
|
+
return constructGhostTunnelUrl({
|
|
521
|
+
domain: input2.domain,
|
|
522
|
+
route: input2.route,
|
|
523
|
+
project: input2.project,
|
|
524
|
+
owner: input2.owner,
|
|
525
|
+
values: input2.values,
|
|
526
|
+
...input2.path ? { path: input2.path } : {},
|
|
527
|
+
...input2.protocol ? { protocol: input2.protocol } : {},
|
|
528
|
+
ghostTunnel: config
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
function resolveGhostTunnelConfig(options, defaults) {
|
|
532
|
+
if (options === false || typeof options === "undefined") {
|
|
533
|
+
return {
|
|
534
|
+
enabled: false,
|
|
535
|
+
mode: DEFAULT_GHOST_TUNNEL_MODE,
|
|
536
|
+
domains: [],
|
|
537
|
+
subdomain: DEFAULT_GHOST_TUNNEL_SUBDOMAIN,
|
|
538
|
+
namespace: resolveNamespaceConfig(void 0),
|
|
539
|
+
displayUrls: [],
|
|
540
|
+
requireHttps: true,
|
|
541
|
+
requireAuth: true
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
const config = typeof options === "string" ? { mode: options } : options;
|
|
545
|
+
const subdomain = config.subdomain ?? DEFAULT_GHOST_TUNNEL_SUBDOMAIN;
|
|
546
|
+
assertValidSubdomain(subdomain);
|
|
547
|
+
const domains = normalizeDomains(config.domains);
|
|
548
|
+
const enabled = config.enabled ?? true;
|
|
549
|
+
const resolved = {
|
|
550
|
+
enabled,
|
|
551
|
+
mode: parseGhostTunnelMode(config.mode),
|
|
552
|
+
domains,
|
|
553
|
+
subdomain,
|
|
554
|
+
namespace: resolveNamespaceConfig(config.namespace),
|
|
555
|
+
...config.preview ? { preview: config.preview } : {},
|
|
556
|
+
displayUrls: [],
|
|
557
|
+
requireHttps: config.requireHttps ?? true,
|
|
558
|
+
requireAuth: config.requireAuth ?? true
|
|
559
|
+
};
|
|
560
|
+
if (!enabled) {
|
|
561
|
+
return resolved;
|
|
562
|
+
}
|
|
563
|
+
const previewUrl = maybeConstructPreviewUrl(resolved, defaults);
|
|
564
|
+
const displayUrls = previewUrl ? [previewUrl] : createDisplayUrls(resolved, defaults);
|
|
565
|
+
return {
|
|
566
|
+
...resolved,
|
|
567
|
+
displayUrls,
|
|
568
|
+
...displayUrls[0] ? { displayUrl: displayUrls[0] } : {},
|
|
569
|
+
...previewUrl ? { previewUrl } : {}
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function getGhostTunnelEntryHost(domain, options = {}) {
|
|
573
|
+
const config = toGhostTunnelConfig(options);
|
|
574
|
+
const normalizedDomain = normalizeDomain(domain);
|
|
575
|
+
if (!normalizedDomain) {
|
|
576
|
+
throw new Error(`Invalid ghost tunnel domain: ${domain}`);
|
|
577
|
+
}
|
|
578
|
+
return `${config.subdomain}.${normalizedDomain}`;
|
|
579
|
+
}
|
|
580
|
+
function constructGhostTunnelHost(input2) {
|
|
581
|
+
const config = toGhostTunnelConfig(input2.ghostTunnel ?? {});
|
|
582
|
+
if (!config.enabled) {
|
|
583
|
+
throw new Error("Ghost tunnel is not enabled.");
|
|
584
|
+
}
|
|
585
|
+
const namespaceValues = {
|
|
586
|
+
route: input2.route,
|
|
587
|
+
project: input2.project,
|
|
588
|
+
owner: input2.owner,
|
|
589
|
+
...input2.values ?? {}
|
|
590
|
+
};
|
|
591
|
+
const slug = createNamespaceSlug(config.namespace, namespaceValues);
|
|
592
|
+
return `${slug}.${getGhostTunnelEntryHost(input2.domain, config)}`;
|
|
593
|
+
}
|
|
594
|
+
function constructGhostTunnelUrl(input2) {
|
|
595
|
+
const protocol = input2.protocol ?? "https";
|
|
596
|
+
const host = constructGhostTunnelHost(input2);
|
|
597
|
+
const url = new URL(`${protocol}://${host}/`);
|
|
598
|
+
if (input2.path) {
|
|
599
|
+
url.pathname = `/${input2.path.replace(/^\/+/, "")}`;
|
|
600
|
+
}
|
|
601
|
+
if (input2.searchParams instanceof URLSearchParams) {
|
|
602
|
+
url.search = input2.searchParams.toString();
|
|
603
|
+
} else if (input2.searchParams) {
|
|
604
|
+
for (const [key, value] of Object.entries(input2.searchParams)) {
|
|
605
|
+
if (typeof value !== "undefined" && value !== null) {
|
|
606
|
+
url.searchParams.set(key, String(value));
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return url.toString();
|
|
611
|
+
}
|
|
612
|
+
|
|
320
613
|
// src/context.ts
|
|
321
614
|
var LOCALGHOST_PROJECT_CONFIG_FILES = [
|
|
322
615
|
"localghost.config.mjs",
|
|
@@ -341,6 +634,25 @@ function envHttps() {
|
|
|
341
634
|
if (!value) return void 0;
|
|
342
635
|
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
343
636
|
}
|
|
637
|
+
function getPackageName(cwd) {
|
|
638
|
+
try {
|
|
639
|
+
const pkg = JSON.parse(readFileSync4(join4(cwd, "package.json"), "utf8"));
|
|
640
|
+
return typeof pkg.name === "string" ? pkg.name : void 0;
|
|
641
|
+
} catch {
|
|
642
|
+
return void 0;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
function getPackageOwner(cwd) {
|
|
646
|
+
const packageName = getPackageName(cwd);
|
|
647
|
+
if (!packageName?.startsWith("@")) return void 0;
|
|
648
|
+
return packageName.slice(1).split("/")[0];
|
|
649
|
+
}
|
|
650
|
+
function getLocalOwner(cwd) {
|
|
651
|
+
return sanitizeProjectName(process.env.LOCALGHOST_OWNER ?? getPackageOwner(cwd) ?? process.env.USER ?? process.env.USERNAME ?? "local");
|
|
652
|
+
}
|
|
653
|
+
function getRouteName(primaryHost, fallback) {
|
|
654
|
+
return sanitizeProjectName(primaryHost.split(".")[0] ?? fallback);
|
|
655
|
+
}
|
|
344
656
|
function readOptionsFromContext(options) {
|
|
345
657
|
return {
|
|
346
658
|
cwd: options.cwd ?? process.cwd(),
|
|
@@ -379,18 +691,22 @@ function addDefaultWwwAliases(entries) {
|
|
|
379
691
|
function defined(input2) {
|
|
380
692
|
return Object.fromEntries(Object.entries(input2).filter(([, value]) => typeof value !== "undefined"));
|
|
381
693
|
}
|
|
382
|
-
async function
|
|
383
|
-
|
|
384
|
-
|
|
694
|
+
async function readLocalghostProjectConfig(options = {}) {
|
|
695
|
+
const cwd = options.cwd ?? process.cwd();
|
|
696
|
+
if (options.configFile === false) return { config: {} };
|
|
697
|
+
const candidates = options.configFile ? [options.configFile] : LOCALGHOST_PROJECT_CONFIG_FILES;
|
|
385
698
|
const path = candidates.map((candidate) => resolveDevHostsPath({ cwd, fileName: candidate }).path).find((candidate) => existsSync3(candidate));
|
|
386
|
-
if (!path) return {};
|
|
699
|
+
if (!path) return { config: {} };
|
|
387
700
|
const imported = await import(`${pathToFileURL(path).href}?localghost=${Date.now()}`);
|
|
388
701
|
const config = imported.default ?? imported;
|
|
389
702
|
return { config, path };
|
|
390
703
|
}
|
|
391
704
|
async function resolveLocalghostContext(options = {}) {
|
|
392
705
|
const cwd = options.cwd ?? process.cwd();
|
|
393
|
-
const projectConfig = await
|
|
706
|
+
const projectConfig = await readLocalghostProjectConfig({
|
|
707
|
+
cwd,
|
|
708
|
+
...typeof options.localghostConfig !== "undefined" ? { configFile: options.localghostConfig } : {}
|
|
709
|
+
});
|
|
394
710
|
const merged = {
|
|
395
711
|
...projectConfig.config,
|
|
396
712
|
...defined(options)
|
|
@@ -399,7 +715,7 @@ async function resolveLocalghostContext(options = {}) {
|
|
|
399
715
|
const resolvedPath = resolveDevHostsPath(readOptions);
|
|
400
716
|
const configEntries = readDevHosts(readOptions);
|
|
401
717
|
const requestedPort = merged.port ?? envPort() ?? configEntries[0]?.port ?? 5173;
|
|
402
|
-
const dynamicPort = merged.dynamicPort ?? envDynamicPort() ??
|
|
718
|
+
const dynamicPort = merged.dynamicPort ?? envDynamicPort() ?? true;
|
|
403
719
|
const bindHost = merged.bindHost ?? "127.0.0.1";
|
|
404
720
|
const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
|
|
405
721
|
const port = dynamicPort ? await findAvailablePort(requestedPort, { host: probeHost }) : requestedPort;
|
|
@@ -407,9 +723,15 @@ async function resolveLocalghostContext(options = {}) {
|
|
|
407
723
|
const entries = wwwAlias ? addDefaultWwwAliases(withRuntimePort(configEntries, requestedPort, port)) : withRuntimePort(configEntries, requestedPort, port);
|
|
408
724
|
const hosts = uniqueHosts(entries);
|
|
409
725
|
const primaryHost = merged.primaryHost ?? entries.find((entry) => entry.port === port)?.host ?? hosts[0] ?? `${sanitizeProjectName(getProjectName(cwd))}.localhost`;
|
|
726
|
+
const projectName = sanitizeProjectName(merged.project ?? getProjectName(cwd));
|
|
727
|
+
const ghostTunnel = resolveGhostTunnelConfig(merged.ghostTunnel, {
|
|
728
|
+
route: getRouteName(primaryHost, projectName),
|
|
729
|
+
project: projectName,
|
|
730
|
+
owner: getLocalOwner(cwd)
|
|
731
|
+
});
|
|
410
732
|
return {
|
|
411
733
|
cwd,
|
|
412
|
-
projectName
|
|
734
|
+
projectName,
|
|
413
735
|
readOptions,
|
|
414
736
|
configPath: resolvedPath.path,
|
|
415
737
|
configFileName: resolvedPath.fileName,
|
|
@@ -423,6 +745,7 @@ async function resolveLocalghostContext(options = {}) {
|
|
|
423
745
|
primaryHost,
|
|
424
746
|
https: merged.https ?? envHttps() ?? false,
|
|
425
747
|
wwwAlias,
|
|
748
|
+
ghostTunnel,
|
|
426
749
|
...projectConfig.path ? { projectConfigPath: projectConfig.path } : {}
|
|
427
750
|
};
|
|
428
751
|
}
|
|
@@ -473,7 +796,7 @@ function assertLocalDevelopment(command, env = process.env) {
|
|
|
473
796
|
// src/hosts-file.ts
|
|
474
797
|
import { writeFileSync as writeFileSync3 } from "fs";
|
|
475
798
|
import { tmpdir } from "os";
|
|
476
|
-
import { join as
|
|
799
|
+
import { join as join5 } from "path";
|
|
477
800
|
import { execa as execa3 } from "execa";
|
|
478
801
|
function escapeRegExp(value) {
|
|
479
802
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -515,7 +838,7 @@ function removeManagedBlock(existing, projectName) {
|
|
|
515
838
|
}
|
|
516
839
|
async function writeSystemHostsFile(hostsPath, next, projectName) {
|
|
517
840
|
const sanitizedProjectName = sanitizeProjectName(projectName);
|
|
518
|
-
const tempPath =
|
|
841
|
+
const tempPath = join5(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
|
|
519
842
|
writeFileSync3(tempPath, next, "utf8");
|
|
520
843
|
if (process.platform === "win32") {
|
|
521
844
|
throw new Error(`Windows support: run as administrator and copy ${tempPath} to ${hostsPath}.`);
|
|
@@ -548,11 +871,11 @@ async function removeSystemHosts(projectName) {
|
|
|
548
871
|
}
|
|
549
872
|
|
|
550
873
|
// src/init.ts
|
|
551
|
-
import { existsSync as existsSync4, readFileSync as
|
|
552
|
-
import { join as
|
|
874
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
875
|
+
import { join as join6 } from "path";
|
|
553
876
|
function detectPackageManager(cwd = process.cwd()) {
|
|
554
|
-
if (existsSync4(
|
|
555
|
-
if (existsSync4(
|
|
877
|
+
if (existsSync4(join6(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
878
|
+
if (existsSync4(join6(cwd, "yarn.lock"))) return "yarn";
|
|
556
879
|
return "npm";
|
|
557
880
|
}
|
|
558
881
|
function packageRunCommand(packageManager, script) {
|
|
@@ -572,7 +895,7 @@ function renderConfig(options) {
|
|
|
572
895
|
}
|
|
573
896
|
function readPackageJson(path) {
|
|
574
897
|
try {
|
|
575
|
-
return JSON.parse(
|
|
898
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
576
899
|
} catch {
|
|
577
900
|
return null;
|
|
578
901
|
}
|
|
@@ -624,7 +947,7 @@ function initLocalghost(options = {}) {
|
|
|
624
947
|
const apiPort = options.apiPort ?? 8787;
|
|
625
948
|
const packageManager = options.packageManager ?? detectPackageManager(cwd);
|
|
626
949
|
const configFile = options.configFile ?? LOCALGHOST_CONFIG_FILE;
|
|
627
|
-
const configPath =
|
|
950
|
+
const configPath = join6(cwd, configFile);
|
|
628
951
|
const configExists = existsSync4(configPath);
|
|
629
952
|
if (configExists && !options.force) {
|
|
630
953
|
return {
|
|
@@ -641,7 +964,7 @@ function initLocalghost(options = {}) {
|
|
|
641
964
|
};
|
|
642
965
|
}
|
|
643
966
|
writeTextFile(configPath, renderConfig({ host, port, apiHost, apiPort }));
|
|
644
|
-
const packageJsonPath =
|
|
967
|
+
const packageJsonPath = join6(cwd, "package.json");
|
|
645
968
|
const packageJsonChanged = options.writeScripts ? updatePackageScripts(packageJsonPath, configFile) : false;
|
|
646
969
|
return {
|
|
647
970
|
configPath,
|
|
@@ -682,6 +1005,20 @@ async function confirm(question, defaultValue = true) {
|
|
|
682
1005
|
}
|
|
683
1006
|
|
|
684
1007
|
// src/routes.ts
|
|
1008
|
+
var ansi = {
|
|
1009
|
+
cyan: "\x1B[36m",
|
|
1010
|
+
dim: "\x1B[2m",
|
|
1011
|
+
green: "\x1B[32m",
|
|
1012
|
+
reset: "\x1B[0m",
|
|
1013
|
+
yellow: "\x1B[33m"
|
|
1014
|
+
};
|
|
1015
|
+
function colorize(value, color, enabled) {
|
|
1016
|
+
return enabled ? `${color}${value}${ansi.reset}` : value;
|
|
1017
|
+
}
|
|
1018
|
+
function colorizeUrl(value, enabled) {
|
|
1019
|
+
if (!enabled) return value;
|
|
1020
|
+
return colorize(value.replace(/\*/g, `${ansi.yellow}*${ansi.cyan}`), ansi.cyan, enabled);
|
|
1021
|
+
}
|
|
685
1022
|
function getDomainRoutes(entries, options = {}) {
|
|
686
1023
|
const protocol = options.https === true ? "https" : "http";
|
|
687
1024
|
return [...entries].sort((left, right) => left.host.localeCompare(right.host) || left.port - right.port).map((entry) => ({
|
|
@@ -701,13 +1038,40 @@ function formatDomainRoutes(entries, options = {}) {
|
|
|
701
1038
|
...routes.map((route) => ` ${route.url} -> ${route.upstream}`)
|
|
702
1039
|
].join("\n");
|
|
703
1040
|
}
|
|
1041
|
+
function formatGhostTunnel(config, options = {}) {
|
|
1042
|
+
if (!config.enabled) return null;
|
|
1043
|
+
const color = options.color === true;
|
|
1044
|
+
const label = options.label ?? "expected";
|
|
1045
|
+
const labelColor = label === "running" ? ansi.green : ansi.dim;
|
|
1046
|
+
const lines = [
|
|
1047
|
+
"localghost ghost tunnel",
|
|
1048
|
+
` mode: ${config.mode}`
|
|
1049
|
+
];
|
|
1050
|
+
const urls = config.displayUrls.length > 0 ? config.displayUrls : config.displayUrl ? [config.displayUrl] : [];
|
|
1051
|
+
if (urls.length === 0) {
|
|
1052
|
+
lines.push(` ${label}: unavailable`);
|
|
1053
|
+
} else if (urls.length === 1) {
|
|
1054
|
+
lines.push(` ${colorize(label, labelColor, color)}: ${colorizeUrl(urls[0], color)}`);
|
|
1055
|
+
} else {
|
|
1056
|
+
lines.push(` ${colorize(label, labelColor, color)}:`);
|
|
1057
|
+
for (const url of urls) {
|
|
1058
|
+
lines.push(` ${colorizeUrl(url, color)}`);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
if (options.verbose) {
|
|
1062
|
+
lines.push(` domains: ${config.domains.length > 0 ? config.domains.join(", ") : "*"}`);
|
|
1063
|
+
lines.push(` access: ${config.requireAuth ? "auth required" : "app decides"}`);
|
|
1064
|
+
lines.push(` transport: ${config.requireHttps ? "https required" : "http allowed"}`);
|
|
1065
|
+
}
|
|
1066
|
+
return lines.join("\n");
|
|
1067
|
+
}
|
|
704
1068
|
|
|
705
1069
|
// src/state.ts
|
|
706
1070
|
import { existsSync as existsSync5 } from "fs";
|
|
707
|
-
import { join as
|
|
1071
|
+
import { join as join7 } from "path";
|
|
708
1072
|
var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
|
|
709
1073
|
function getLocalghostStatePath(cwd = process.cwd()) {
|
|
710
|
-
return
|
|
1074
|
+
return join7(cwd, LOCALGHOST_STATE_FILE);
|
|
711
1075
|
}
|
|
712
1076
|
function readLocalghostState(cwd = process.cwd()) {
|
|
713
1077
|
const path = getLocalghostStatePath(cwd);
|
|
@@ -727,11 +1091,11 @@ function patchLocalghostState(cwd, patch) {
|
|
|
727
1091
|
}
|
|
728
1092
|
|
|
729
1093
|
// src/update-check.ts
|
|
730
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as
|
|
1094
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
731
1095
|
import { homedir as homedir2 } from "os";
|
|
732
|
-
import { dirname as dirname4, join as
|
|
1096
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
733
1097
|
var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
|
|
734
|
-
var LOCALGHOST_VERSION = "0.1.
|
|
1098
|
+
var LOCALGHOST_VERSION = "0.1.10";
|
|
735
1099
|
var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
736
1100
|
var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
737
1101
|
var UPDATE_CHECK_TIMEOUT_MS = 900;
|
|
@@ -743,13 +1107,13 @@ function isUpdateCheckDisabled(env = process.env) {
|
|
|
743
1107
|
}
|
|
744
1108
|
function getUpdateCheckCachePath(env = process.env) {
|
|
745
1109
|
if (env.LOCALGHOST_UPDATE_CHECK_CACHE) return env.LOCALGHOST_UPDATE_CHECK_CACHE;
|
|
746
|
-
const cacheRoot = env.XDG_CACHE_HOME ||
|
|
747
|
-
return
|
|
1110
|
+
const cacheRoot = env.XDG_CACHE_HOME || join8(homedir2(), ".cache");
|
|
1111
|
+
return join8(cacheRoot, "localghost", "update-check.json");
|
|
748
1112
|
}
|
|
749
1113
|
function readCache(path = getUpdateCheckCachePath()) {
|
|
750
1114
|
if (!existsSync6(path)) return null;
|
|
751
1115
|
try {
|
|
752
|
-
return JSON.parse(
|
|
1116
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
753
1117
|
} catch {
|
|
754
1118
|
return null;
|
|
755
1119
|
}
|
|
@@ -903,8 +1267,18 @@ function warnAboutLocalMdns(entries) {
|
|
|
903
1267
|
);
|
|
904
1268
|
}
|
|
905
1269
|
}
|
|
1270
|
+
function shouldColor() {
|
|
1271
|
+
return process.stdout.isTTY && !process.env.NO_COLOR;
|
|
1272
|
+
}
|
|
906
1273
|
function logDomainRoutes(entries, options = {}) {
|
|
907
1274
|
console.log(formatDomainRoutes(entries, options));
|
|
1275
|
+
if (options.ghostTunnel?.enabled) {
|
|
1276
|
+
console.log(formatGhostTunnel(options.ghostTunnel, {
|
|
1277
|
+
color: shouldColor(),
|
|
1278
|
+
label: "expected",
|
|
1279
|
+
verbose: options.verbose === true
|
|
1280
|
+
}));
|
|
1281
|
+
}
|
|
908
1282
|
}
|
|
909
1283
|
function parsePort2(value) {
|
|
910
1284
|
const port = Number.parseInt(value, 10);
|
|
@@ -998,7 +1372,7 @@ function getSetupReadiness(options) {
|
|
|
998
1372
|
}
|
|
999
1373
|
const hostsPath = getSystemHostsPath();
|
|
1000
1374
|
try {
|
|
1001
|
-
const hosts =
|
|
1375
|
+
const hosts = readFileSync7(hostsPath, "utf8");
|
|
1002
1376
|
const expectedHostsBlock = renderHostsBlock(projectName, entries).trimEnd();
|
|
1003
1377
|
if (!hosts.includes(expectedHostsBlock)) {
|
|
1004
1378
|
reasons.push(`The Localghost hosts block in ${hostsPath} is missing or stale.`);
|
|
@@ -1012,7 +1386,7 @@ function getSetupReadiness(options) {
|
|
|
1012
1386
|
reasons.push(`Missing Caddyfile at ${caddyfilePath}.`);
|
|
1013
1387
|
} else {
|
|
1014
1388
|
const expectedCaddyfile = renderCaddyfile(entries, { https });
|
|
1015
|
-
const currentCaddyfile =
|
|
1389
|
+
const currentCaddyfile = readFileSync7(caddyfilePath, "utf8");
|
|
1016
1390
|
if (currentCaddyfile !== expectedCaddyfile) {
|
|
1017
1391
|
reasons.push(`Caddyfile at ${caddyfilePath} is stale for ${https ? "HTTPS" : "HTTP"} mode.`);
|
|
1018
1392
|
}
|
|
@@ -1047,6 +1421,14 @@ async function runSetupFromReadiness(cwd, https, readiness) {
|
|
|
1047
1421
|
...existingTrustMarkers(cwd),
|
|
1048
1422
|
entries: readiness.entries
|
|
1049
1423
|
});
|
|
1424
|
+
registerLocalghostSetup({
|
|
1425
|
+
cwd,
|
|
1426
|
+
projectName: readiness.projectName,
|
|
1427
|
+
configPath: readiness.configPath,
|
|
1428
|
+
caddyfilePath,
|
|
1429
|
+
https,
|
|
1430
|
+
entries: readiness.entries
|
|
1431
|
+
});
|
|
1050
1432
|
}
|
|
1051
1433
|
function wait(ms) {
|
|
1052
1434
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
@@ -1096,35 +1478,91 @@ function registerCleanup(id) {
|
|
|
1096
1478
|
process.off("exit", cleanup);
|
|
1097
1479
|
};
|
|
1098
1480
|
}
|
|
1099
|
-
async function
|
|
1481
|
+
async function getRouteViews(entries) {
|
|
1100
1482
|
const portStatus = /* @__PURE__ */ new Map();
|
|
1101
|
-
for (const entry of
|
|
1483
|
+
for (const entry of entries) {
|
|
1102
1484
|
if (!portStatus.has(entry.port)) {
|
|
1103
1485
|
portStatus.set(entry.port, !await isPortAvailable(entry.port));
|
|
1104
1486
|
}
|
|
1105
1487
|
}
|
|
1488
|
+
return entries.map((entry) => ({
|
|
1489
|
+
host: entry.host,
|
|
1490
|
+
port: entry.port,
|
|
1491
|
+
target: `127.0.0.1:${entry.port}`,
|
|
1492
|
+
listening: portStatus.get(entry.port) ?? false
|
|
1493
|
+
}));
|
|
1494
|
+
}
|
|
1495
|
+
function setupKey(input2) {
|
|
1496
|
+
return `${input2.projectName}:${input2.cwd}:${input2.configPath ?? ""}`;
|
|
1497
|
+
}
|
|
1498
|
+
function runKey(input2) {
|
|
1499
|
+
return `${input2.projectName}:${input2.cwd}:${input2.configPath ?? ""}`;
|
|
1500
|
+
}
|
|
1501
|
+
async function getInstanceViews(setups, runs) {
|
|
1502
|
+
const runBySetup = new Map(runs.map((run) => [runKey(run), run]));
|
|
1503
|
+
const instances = [];
|
|
1504
|
+
for (const setup of setups) {
|
|
1505
|
+
const run = runBySetup.get(setupKey(setup));
|
|
1506
|
+
if (run) {
|
|
1507
|
+
instances.push(await getRunInstanceView(run, setup));
|
|
1508
|
+
runBySetup.delete(setupKey(setup));
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
instances.push({
|
|
1512
|
+
id: setup.id,
|
|
1513
|
+
cwd: setup.cwd,
|
|
1514
|
+
projectName: setup.projectName,
|
|
1515
|
+
running: false,
|
|
1516
|
+
mode: "setup",
|
|
1517
|
+
updatedAt: setup.updatedAt,
|
|
1518
|
+
...setup.configPath ? { configPath: setup.configPath } : {},
|
|
1519
|
+
...setup.caddyfilePath ? { caddyfilePath: setup.caddyfilePath } : {},
|
|
1520
|
+
...typeof setup.https === "boolean" ? { https: setup.https } : {},
|
|
1521
|
+
routes: await getRouteViews(setup.entries)
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
for (const run of runBySetup.values()) {
|
|
1525
|
+
instances.push(await getRunInstanceView(run));
|
|
1526
|
+
}
|
|
1527
|
+
return instances.sort((left, right) => {
|
|
1528
|
+
if (left.running !== right.running) return left.running ? -1 : 1;
|
|
1529
|
+
return left.projectName.localeCompare(right.projectName);
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
async function getRunInstanceView(run, setup) {
|
|
1106
1533
|
return {
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1534
|
+
id: setup?.id ?? run.id,
|
|
1535
|
+
cwd: run.cwd,
|
|
1536
|
+
projectName: run.projectName,
|
|
1537
|
+
running: true,
|
|
1538
|
+
mode: run.mode,
|
|
1539
|
+
updatedAt: setup?.updatedAt ?? run.updatedAt,
|
|
1540
|
+
startedAt: run.startedAt,
|
|
1541
|
+
pid: run.pid,
|
|
1542
|
+
...run.caddyPid ? { caddyPid: run.caddyPid } : {},
|
|
1543
|
+
...run.childPid ? { childPid: run.childPid } : {},
|
|
1544
|
+
...run.childCommand ? { childCommand: run.childCommand } : {},
|
|
1545
|
+
...run.configPath ? { configPath: run.configPath } : {},
|
|
1546
|
+
...run.caddyfilePath ? { caddyfilePath: run.caddyfilePath } : {},
|
|
1547
|
+
...typeof run.https === "boolean" ? { https: run.https } : {},
|
|
1548
|
+
routes: await getRouteViews(run.entries)
|
|
1114
1549
|
};
|
|
1115
1550
|
}
|
|
1116
|
-
function
|
|
1117
|
-
if (
|
|
1551
|
+
function formatInstanceViews(instances) {
|
|
1552
|
+
if (instances.length === 0) return "No Localghost setups found.";
|
|
1118
1553
|
const lines = ["localghost ps"];
|
|
1119
|
-
for (const
|
|
1120
|
-
const command =
|
|
1121
|
-
const mode = command ? `${
|
|
1554
|
+
for (const instance of instances) {
|
|
1555
|
+
const command = instance.childCommand?.length ? ` ${instance.childCommand.join(" ")}` : "";
|
|
1556
|
+
const mode = command ? `${instance.mode}:${command}` : instance.mode === "setup" ? "" : instance.mode;
|
|
1122
1557
|
lines.push("");
|
|
1123
|
-
lines.push(`${
|
|
1124
|
-
lines.push(` cwd: ${
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1558
|
+
lines.push(`${instance.projectName} ${instance.running ? "running" : "setup"}${mode ? ` ${mode}` : ""}`);
|
|
1559
|
+
lines.push(` cwd: ${instance.cwd}`);
|
|
1560
|
+
if (instance.pid) {
|
|
1561
|
+
lines.push(` pid: ${instance.pid}${instance.caddyPid ? `, caddy: ${instance.caddyPid}` : ""}${instance.childPid ? `, child: ${instance.childPid}` : ""}`);
|
|
1562
|
+
}
|
|
1563
|
+
if (instance.startedAt) lines.push(` started: ${instance.startedAt}`);
|
|
1564
|
+
if (!instance.startedAt && instance.updatedAt) lines.push(` setup: ${instance.updatedAt}`);
|
|
1565
|
+
for (const route of instance.routes) {
|
|
1128
1566
|
lines.push(` ${route.host} -> ${route.target} (${route.listening ? "listening" : "not listening"})`);
|
|
1129
1567
|
}
|
|
1130
1568
|
}
|
|
@@ -1198,7 +1636,7 @@ program.command("setup").description("Update /etc/hosts and generate/validate Ca
|
|
|
1198
1636
|
const configPath = context.configPath;
|
|
1199
1637
|
const entries = context.entries;
|
|
1200
1638
|
warnAboutLocalMdns(entries);
|
|
1201
|
-
logDomainRoutes(entries, { https });
|
|
1639
|
+
logDomainRoutes(entries, { https, ghostTunnel: context.ghostTunnel });
|
|
1202
1640
|
explainHostsPassword();
|
|
1203
1641
|
const hostsResult = await updateSystemHosts(projectName, entries);
|
|
1204
1642
|
if (hostsResult.changed) {
|
|
@@ -1221,6 +1659,14 @@ program.command("setup").description("Update /etc/hosts and generate/validate Ca
|
|
|
1221
1659
|
...existingTrustMarkers(options.cwd),
|
|
1222
1660
|
entries
|
|
1223
1661
|
});
|
|
1662
|
+
registerLocalghostSetup({
|
|
1663
|
+
cwd: options.cwd,
|
|
1664
|
+
projectName,
|
|
1665
|
+
configPath,
|
|
1666
|
+
caddyfilePath: caddyfile,
|
|
1667
|
+
https,
|
|
1668
|
+
entries
|
|
1669
|
+
});
|
|
1224
1670
|
console.log(`Generated ${caddyfile}`);
|
|
1225
1671
|
console.log(`Mode ${https ? "HTTPS" : "HTTP"}`);
|
|
1226
1672
|
console.log(`State ${statePath}`);
|
|
@@ -1234,7 +1680,7 @@ program.command("trust").description("Trust Caddy's local HTTPS CA for this proj
|
|
|
1234
1680
|
throw new Error("Localghost HTTPS is not enabled for this context. Set https: true in localghost.config.mjs or pass --https.");
|
|
1235
1681
|
}
|
|
1236
1682
|
warnAboutLocalMdns(context.entries);
|
|
1237
|
-
logDomainRoutes(context.entries, { https: true });
|
|
1683
|
+
logDomainRoutes(context.entries, { https: true, ghostTunnel: context.ghostTunnel });
|
|
1238
1684
|
explainTrustPassword();
|
|
1239
1685
|
const caddyfile = await writeCaddyfile(context.entries, options.cwd, { https: true });
|
|
1240
1686
|
await validateCaddyfile(caddyfile);
|
|
@@ -1264,6 +1710,7 @@ program.command("reset").description("Remove Localghost setup state without dele
|
|
|
1264
1710
|
} else {
|
|
1265
1711
|
console.log(`No Localghost hosts block found in ${hostsResult.hostsPath}`);
|
|
1266
1712
|
}
|
|
1713
|
+
unregisterLocalghostSetup({ cwd: options.cwd, projectName });
|
|
1267
1714
|
console.log(".localghost was left in place. Run localghost setup when you are ready.");
|
|
1268
1715
|
});
|
|
1269
1716
|
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 +1742,7 @@ program.command("teardown").description("Remove Localghost's managed /etc/hosts
|
|
|
1295
1742
|
if (options.removeCaddyfile) {
|
|
1296
1743
|
console.log(caddyfileRemoved ? `Removed ${caddyfilePath}` : `${caddyfilePath} was not present`);
|
|
1297
1744
|
}
|
|
1745
|
+
unregisterLocalghostSetup({ cwd: options.cwd, projectName });
|
|
1298
1746
|
console.log(`State ${statePath}`);
|
|
1299
1747
|
});
|
|
1300
1748
|
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 +1788,17 @@ program.command("status").description("Print Localghost's project-local state fi
|
|
|
1340
1788
|
process.exitCode = 1;
|
|
1341
1789
|
}
|
|
1342
1790
|
});
|
|
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) => {
|
|
1791
|
+
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
1792
|
const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
|
|
1345
1793
|
warnAboutLocalMdns(context.entries);
|
|
1346
1794
|
console.log(formatDomainRoutes(context.entries, { https: options.http ? false : context.https }));
|
|
1795
|
+
if (context.ghostTunnel.enabled) {
|
|
1796
|
+
console.log(formatGhostTunnel(context.ghostTunnel, {
|
|
1797
|
+
color: shouldColor(),
|
|
1798
|
+
label: "expected",
|
|
1799
|
+
verbose: options.verbose === true
|
|
1800
|
+
}));
|
|
1801
|
+
}
|
|
1347
1802
|
});
|
|
1348
1803
|
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
1804
|
assertLocalDevelopment("dev");
|
|
@@ -1385,9 +1840,17 @@ program.command("dev").description("Run the Localghost Caddy proxy after setup")
|
|
|
1385
1840
|
...existingTrustMarkers(options.cwd),
|
|
1386
1841
|
entries: readiness.entries
|
|
1387
1842
|
});
|
|
1843
|
+
registerLocalghostSetup({
|
|
1844
|
+
cwd: options.cwd,
|
|
1845
|
+
projectName: readiness.projectName,
|
|
1846
|
+
configPath: readiness.configPath,
|
|
1847
|
+
caddyfilePath,
|
|
1848
|
+
https,
|
|
1849
|
+
entries: readiness.entries
|
|
1850
|
+
});
|
|
1388
1851
|
}
|
|
1389
1852
|
warnAboutLocalMdns(readiness.entries);
|
|
1390
|
-
logDomainRoutes(readiness.entries, { https });
|
|
1853
|
+
logDomainRoutes(readiness.entries, { https, ghostTunnel: context.ghostTunnel });
|
|
1391
1854
|
const caddyfile = await writeCaddyfile(readiness.entries, options.cwd, { https });
|
|
1392
1855
|
await validateCaddyfile(caddyfile);
|
|
1393
1856
|
const caddy = startCaddy(caddyfile);
|
|
@@ -1420,7 +1883,7 @@ program.command("dev").description("Run the Localghost Caddy proxy after setup")
|
|
|
1420
1883
|
cleanupRun();
|
|
1421
1884
|
}
|
|
1422
1885
|
});
|
|
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
|
|
1886
|
+
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
1887
|
assertLocalDevelopment("run");
|
|
1425
1888
|
await assertCaddyReady();
|
|
1426
1889
|
const context = await resolveLocalghostContext({
|
|
@@ -1459,7 +1922,7 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
|
|
|
1459
1922
|
console.log(`Port ${context.requestedPort} is busy; using ${context.port}.`);
|
|
1460
1923
|
}
|
|
1461
1924
|
warnAboutLocalMdns(context.entries);
|
|
1462
|
-
logDomainRoutes(context.entries, { https });
|
|
1925
|
+
logDomainRoutes(context.entries, { https, ghostTunnel: context.ghostTunnel });
|
|
1463
1926
|
const caddyfile = await writeCaddyfile(context.entries, options.cwd, { https });
|
|
1464
1927
|
await validateCaddyfile(caddyfile);
|
|
1465
1928
|
const caddy = startCaddy(caddyfile);
|
|
@@ -1524,13 +1987,15 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
|
|
|
1524
1987
|
cleanupRun();
|
|
1525
1988
|
}
|
|
1526
1989
|
});
|
|
1527
|
-
program.command("ps").description("Show Localghost
|
|
1528
|
-
const
|
|
1990
|
+
program.command("ps").description("Show Localghost setups and currently running sessions").option("--json", "Print raw JSON").action(async (options) => {
|
|
1991
|
+
const setups = listLocalghostSetups();
|
|
1992
|
+
const runs = listLocalghostRuns();
|
|
1993
|
+
const instances = await getInstanceViews(setups, runs);
|
|
1529
1994
|
if (options.json) {
|
|
1530
|
-
console.log(JSON.stringify({ activityPath: getLocalghostActivityPath(), runs }, null, 2));
|
|
1995
|
+
console.log(JSON.stringify({ activityPath: getLocalghostActivityPath(), setups, runs, instances }, null, 2));
|
|
1531
1996
|
return;
|
|
1532
1997
|
}
|
|
1533
|
-
console.log(
|
|
1998
|
+
console.log(formatInstanceViews(instances));
|
|
1534
1999
|
});
|
|
1535
2000
|
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
2001
|
const entries = readDevHosts(readOptionsFromCli(options));
|